Coverage for src/local_deep_research/api/research_functions.py: 93%

228 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-20 01:24 +0000

1""" 

2API module for Local Deep Research. 

3Provides programmatic access to search and research capabilities. 

4""" 

5 

6from datetime import datetime, UTC 

7from typing import Any, Callable 

8 

9from loguru import logger 

10from local_deep_research.settings.logger import log_settings 

11 

12from ..config.llm_config import get_llm 

13from ..config.search_config import get_search 

14from ..config.thread_settings import get_setting_from_snapshot 

15from ..report_generator import IntegratedReportGenerator 

16from ..search_system import AdvancedSearchSystem 

17from ..utilities.db_utils import no_db_settings 

18from ..utilities.thread_context import clear_search_context, set_search_context 

19from .settings_utils import create_settings_snapshot 

20 

21 

22def _close_system(system): 

23 """Close an AdvancedSearchSystem and its associated resources.""" 

24 from ..utilities.resource_utils import safe_close 

25 

26 safe_close(system, "search system") 

27 if hasattr(system, "search"): 27 ↛ 29line 27 didn't jump to line 29 because the condition on line 27 was always true

28 safe_close(system.search, "search engine") 

29 if hasattr(system, "model"): 29 ↛ exitline 29 didn't return from function '_close_system' because the condition on line 29 was always true

30 safe_close(system.model, "system LLM") 

31 

32 

33def _init_search_system( 

34 model_name: str | None = None, 

35 temperature: float = 0.7, 

36 provider: str | None = None, 

37 openai_endpoint_url: str | None = None, 

38 progress_callback: Callable[[str, int, dict], None] | None = None, 

39 search_tool: str | None = None, 

40 search_strategy: str = "source_based", 

41 iterations: int = 1, 

42 questions_per_iteration: int = 1, 

43 retrievers: dict[str, Any] | None = None, 

44 llms: dict[str, Any] | None = None, 

45 username: str | None = None, 

46 research_id: str | None = None, 

47 research_context: dict[str, Any] | None = None, 

48 programmatic_mode: bool = True, 

49 search_original_query: bool = True, 

50 settings_snapshot: dict[str, Any] | None = None, 

51 **kwargs: Any, 

52) -> AdvancedSearchSystem: 

53 """ 

54 Initializes the advanced search system with specified parameters. This function sets up 

55 and returns an instance of the AdvancedSearchSystem using the provided configuration 

56 options such as model name, temperature for randomness in responses, provider service 

57 details, endpoint URL, and an optional search tool. 

58 

59 Args: 

60 model_name: Name of the model to use (if None, uses database setting) 

61 temperature: LLM temperature for generation 

62 provider: Provider to use (if None, uses database setting) 

63 openai_endpoint_url: Custom endpoint URL to use (if None, uses database 

64 setting) 

65 progress_callback: Optional callback function to receive progress updates 

66 search_tool: Search engine to use (searxng, wikipedia, arxiv, etc.). If None, uses default 

67 search_strategy: Search strategy to use (modular, source_based, etc.). If None, uses default 

68 iterations: Number of research cycles to perform 

69 questions_per_iteration: Number of questions to generate per cycle 

70 search_strategy: The name of the search strategy to use. 

71 retrievers: Optional dictionary of {name: retriever} pairs to use as search engines 

72 llms: Optional dictionary of {name: llm} pairs to use as language models 

73 programmatic_mode: If True, disables database operations and metrics tracking 

74 search_original_query: Whether to include the original query in the first iteration of search 

75 

76 Returns: 

77 AdvancedSearchSystem: An instance of the configured AdvancedSearchSystem. 

78 

79 """ 

80 # Register retrievers if provided 

81 if retrievers: 

82 from ..web_search_engines.retriever_registry import retriever_registry 

83 

84 retriever_registry.register_multiple(retrievers) 

85 logger.info( 

86 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}" 

87 ) 

88 

89 # Register LLMs if provided 

90 if llms: 

91 from ..llm import register_llm 

92 

93 for name, llm_instance in llms.items(): 

94 register_llm(name, llm_instance) 

95 logger.info(f"Registered {len(llms)} LLMs: {list(llms.keys())}") 

96 

97 # Use settings_snapshot from parameter, or fall back to kwargs 

98 if settings_snapshot is None: 

99 settings_snapshot = kwargs.get("settings_snapshot") 

100 

101 # Get language model with custom temperature 

102 llm = get_llm( 

103 temperature=temperature, 

104 openai_endpoint_url=openai_endpoint_url, 

105 model_name=model_name, 

106 provider=provider, 

107 research_id=research_id, 

108 research_context=research_context, 

109 settings_snapshot=settings_snapshot, 

110 ) 

111 

112 # Set the search engine if specified or get from settings 

113 search_engine = None 

114 

115 try: 

116 # If no search_tool provided, get from settings_snapshot 

117 if not search_tool and settings_snapshot: 

118 search_tool = get_setting_from_snapshot( 

119 "search.tool", settings_snapshot=settings_snapshot 

120 ) 

121 

122 if search_tool: 

123 search_engine = get_search( 

124 search_tool, 

125 llm_instance=llm, 

126 username=username, 

127 settings_snapshot=settings_snapshot, 

128 programmatic_mode=programmatic_mode, 

129 ) 

130 if search_engine is None: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 logger.warning( 

132 f"Could not create search engine '{search_tool}', using default." 

133 ) 

134 

135 # Create search system with custom parameters 

136 logger.info("Search strategy: {}", search_strategy) 

137 system = AdvancedSearchSystem( 

138 llm=llm, 

139 search=search_engine, 

140 strategy_name=search_strategy, 

141 username=username, 

142 research_id=research_id, 

143 research_context=research_context, 

144 settings_snapshot=settings_snapshot, 

145 programmatic_mode=programmatic_mode, 

146 search_original_query=search_original_query, 

147 ) 

148 except Exception: 

149 from ..utilities.resource_utils import safe_close 

150 

151 safe_close(llm, "init LLM") 

152 raise 

153 

154 # Override default settings with user-provided values 

155 system.max_iterations = iterations 

156 system.questions_per_iteration = questions_per_iteration 

157 

158 # Set progress callback if provided 

159 if progress_callback: 

160 system.set_progress_callback(progress_callback) 

161 

162 return system 

163 

164 

165@no_db_settings 

166def quick_summary( 

167 query: str, 

168 research_id: str | None = None, 

169 retrievers: dict[str, Any] | None = None, 

170 llms: dict[str, Any] | None = None, 

171 username: str | None = None, 

172 provider: str | None = None, 

173 api_key: str | None = None, 

174 temperature: float | None = None, 

175 max_search_results: int | None = None, 

176 settings: dict[str, Any] | None = None, 

177 settings_override: dict[str, Any] | None = None, 

178 search_original_query: bool = True, 

179 **kwargs: Any, 

180) -> dict[str, Any]: 

181 """ 

182 Generate a quick research summary for a given query. 

183 

184 Args: 

185 query: The research query to analyze 

186 research_id: Optional research ID (int or UUID string) for tracking metrics 

187 retrievers: Optional dictionary of {name: retriever} pairs to use as search engines 

188 llms: Optional dictionary of {name: llm} pairs to use as language models 

189 provider: LLM provider to use (e.g., 'openai', 'anthropic'). For programmatic API only. 

190 api_key: API key for the provider. For programmatic API only. 

191 temperature: LLM temperature (0.0-1.0). For programmatic API only. 

192 max_search_results: Maximum number of search results to return. For programmatic API only. 

193 settings: Base settings dict to use instead of defaults. For programmatic API only. 

194 settings_override: Dictionary of settings to override (e.g., {"llm.max_tokens": 4000}). For programmatic API only. 

195 search_original_query: Whether to include the original query in the first iteration of search. 

196 Set to False for news searches to avoid sending long subscription prompts to search engines. 

197 **kwargs: Additional configuration for the search system. Will be forwarded to 

198 `_init_search_system()`. 

199 

200 Returns: 

201 Dictionary containing the research results with keys: 

202 - 'summary': The generated summary text 

203 - 'findings': List of detailed findings from each search 

204 - 'iterations': Number of iterations performed 

205 - 'questions': Questions generated during research 

206 

207 Examples: 

208 # Simple usage with defaults 

209 result = quick_summary("What is quantum computing?") 

210 

211 # With custom provider 

212 result = quick_summary( 

213 "What is quantum computing?", 

214 provider="anthropic", 

215 api_key="sk-ant-..." 

216 ) 

217 

218 # With advanced settings 

219 result = quick_summary( 

220 "What is quantum computing?", 

221 temperature=0.2, 

222 settings_override={"search.engines.arxiv.enabled": True} 

223 ) 

224 """ 

225 logger.info("Generating quick summary for query: {}", query) 

226 

227 if "settings_snapshot" not in kwargs: 

228 snapshot_kwargs = {} 

229 if provider is not None: 

230 snapshot_kwargs["provider"] = provider 

231 if api_key is not None: 

232 snapshot_kwargs["api_key"] = api_key 

233 if temperature is not None: 

234 snapshot_kwargs["temperature"] = temperature 

235 if max_search_results is not None: 

236 snapshot_kwargs["max_search_results"] = max_search_results 

237 

238 if ( 

239 not snapshot_kwargs 

240 and settings is None 

241 and settings_override is None 

242 ): 

243 logger.warning( 

244 "No settings_snapshot or explicit config provided to quick_summary(). " 

245 "Using defaults and environment variables. For explicit control, " 

246 "pass settings_snapshot=create_settings_snapshot(...)." 

247 ) 

248 

249 kwargs["settings_snapshot"] = create_settings_snapshot( 

250 base_settings=settings, 

251 overrides=settings_override, 

252 **snapshot_kwargs, 

253 ) 

254 log_settings( 

255 kwargs["settings_snapshot"], 

256 "Created settings snapshot for programmatic API", 

257 ) 

258 else: 

259 log_settings( 

260 kwargs["settings_snapshot"], 

261 "Using provided settings snapshot for programmatic API", 

262 ) 

263 

264 # Generate a research_id if none provided 

265 if research_id is None: 

266 import uuid 

267 

268 research_id = str(uuid.uuid4()) 

269 logger.debug(f"Generated research_id: {research_id}") 

270 

271 # Register retrievers if provided 

272 if retrievers: 

273 from ..web_search_engines.retriever_registry import retriever_registry 

274 

275 retriever_registry.register_multiple(retrievers) 

276 logger.info( 

277 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}" 

278 ) 

279 

280 # Register LLMs if provided 

281 if llms: 

282 from ..llm import register_llm 

283 

284 for name, llm_instance in llms.items(): 

285 register_llm(name, llm_instance) 

286 logger.info(f"Registered {len(llms)} LLMs: {list(llms.keys())}") 

287 

288 search_context = { 

289 "research_id": research_id, # Pass UUID or integer directly 

290 "research_query": query, 

291 "research_mode": kwargs.get("research_mode", "quick"), 

292 "research_phase": "init", 

293 "search_iteration": 0, 

294 "search_engine_selected": kwargs.get("search_tool"), 

295 "username": username, # Include username for metrics tracking 

296 "user_password": kwargs.get( 

297 "user_password" 

298 ), # Include password for metrics tracking 

299 # Thread-safe settings snapshot propagated to background search 

300 # threads (engine config, per-user resolution, egress scope). 

301 "settings_snapshot": kwargs.get("settings_snapshot") or {}, 

302 } 

303 set_search_context(search_context) 

304 

305 system = None 

306 try: 

307 # Remove research_mode from kwargs before passing to _init_search_system 

308 init_kwargs = {k: v for k, v in kwargs.items() if k != "research_mode"} 

309 # Make sure username is passed to the system 

310 init_kwargs["username"] = username 

311 init_kwargs["research_id"] = research_id 

312 init_kwargs["research_context"] = search_context 

313 init_kwargs["search_original_query"] = search_original_query 

314 system = _init_search_system(llms=llms, **init_kwargs) 

315 

316 # Perform the search and analysis 

317 results = system.analyze_topic(query) 

318 

319 # Extract the summary from the current knowledge 

320 if results and "current_knowledge" in results: 

321 summary = results["current_knowledge"] 

322 else: 

323 summary = "Unable to generate summary for the query." 

324 

325 # Prepare the return value (guard against None results) 

326 if results is None: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true

327 results = {} 

328 return { 

329 "research_id": research_id, 

330 "summary": summary, 

331 "findings": results.get("findings", []), 

332 "iterations": results.get("iterations", 0), 

333 "questions": results.get("questions", {}), 

334 "formatted_findings": results.get("formatted_findings", ""), 

335 "sources": results.get("all_links_of_system", []), 

336 } 

337 finally: 

338 if system is not None: 

339 _close_system(system) 

340 clear_search_context() 

341 

342 

343@no_db_settings 

344def generate_report( 

345 query: str, 

346 output_file: str | None = None, 

347 progress_callback: Callable | None = None, 

348 searches_per_section: int = 2, 

349 retrievers: dict[str, Any] | None = None, 

350 llms: dict[str, Any] | None = None, 

351 username: str | None = None, 

352 provider: str | None = None, 

353 api_key: str | None = None, 

354 temperature: float | None = None, 

355 max_search_results: int | None = None, 

356 settings: dict[str, Any] | None = None, 

357 settings_override: dict[str, Any] | None = None, 

358 **kwargs: Any, 

359) -> dict[str, Any]: 

360 """ 

361 Generate a comprehensive, structured research report for a given query. 

362 

363 Args: 

364 query: The research query to analyze 

365 output_file: Optional path to save report markdown file 

366 progress_callback: Optional callback function to receive progress updates 

367 searches_per_section: The number of searches to perform for each 

368 section in the report. 

369 retrievers: Optional dictionary of {name: retriever} pairs to use as search engines 

370 llms: Optional dictionary of {name: llm} pairs to use as language models 

371 provider: LLM provider to use (e.g., 'openai', 'anthropic'). For programmatic API only. 

372 api_key: API key for the provider. For programmatic API only. 

373 temperature: LLM temperature (0.0-1.0). For programmatic API only. 

374 max_search_results: Maximum number of search results to return. For programmatic API only. 

375 settings: Base settings dict to use instead of defaults. For programmatic API only. 

376 settings_override: Dictionary of settings to override. For programmatic API only. 

377 **kwargs: Additional configuration for the search system. 

378 

379 Returns: 

380 Dictionary containing the research report with keys: 

381 - 'content': The full report content in markdown format 

382 - 'metadata': Report metadata including generated timestamp and query 

383 - 'file_path': Path to saved file (if output_file was provided) 

384 

385 Examples: 

386 # Simple usage with settings snapshot 

387 from local_deep_research.api.settings_utils import create_settings_snapshot 

388 settings = create_settings_snapshot({"programmatic_mode": True}) 

389 result = generate_report("AI research", settings_snapshot=settings) 

390 

391 # Save to file 

392 result = generate_report( 

393 "AI research", 

394 output_file="report.md", 

395 settings_snapshot=settings 

396 ) 

397 """ 

398 logger.info("Generating comprehensive research report for query: {}", query) 

399 

400 if "settings_snapshot" not in kwargs: 

401 snapshot_kwargs = {} 

402 if provider is not None: 

403 snapshot_kwargs["provider"] = provider 

404 if api_key is not None: 404 ↛ 405line 404 didn't jump to line 405 because the condition on line 404 was never true

405 snapshot_kwargs["api_key"] = api_key 

406 if temperature is not None: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true

407 snapshot_kwargs["temperature"] = temperature 

408 if max_search_results is not None: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true

409 snapshot_kwargs["max_search_results"] = max_search_results 

410 

411 if ( 

412 not snapshot_kwargs 

413 and settings is None 

414 and settings_override is None 

415 ): 

416 logger.warning( 

417 "No settings_snapshot or explicit config provided to generate_report(). " 

418 "Using defaults and environment variables. For explicit control, " 

419 "pass settings_snapshot=create_settings_snapshot(...)." 

420 ) 

421 

422 kwargs["settings_snapshot"] = create_settings_snapshot( 

423 base_settings=settings, 

424 overrides=settings_override, 

425 **snapshot_kwargs, 

426 ) 

427 log_settings( 

428 kwargs["settings_snapshot"], 

429 "Created settings snapshot for programmatic API", 

430 ) 

431 else: 

432 log_settings( 

433 kwargs["settings_snapshot"], 

434 "Using provided settings snapshot for programmatic API", 

435 ) 

436 

437 # Register retrievers if provided 

438 if retrievers: 

439 from ..web_search_engines.retriever_registry import retriever_registry 

440 

441 retriever_registry.register_multiple(retrievers) 

442 logger.info( 

443 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}" 

444 ) 

445 

446 # Register LLMs if provided 

447 if llms: 

448 from ..llm import register_llm 

449 

450 for name, llm_instance in llms.items(): 

451 register_llm(name, llm_instance) 

452 logger.info(f"Registered {len(llms)} LLMs: {list(llms.keys())}") 

453 

454 import uuid 

455 

456 search_context = { 

457 "research_id": str(uuid.uuid4()), 

458 "research_query": query, 

459 "research_mode": "report", 

460 "research_phase": "init", 

461 "search_iteration": 0, 

462 "search_engine_selected": kwargs.get("search_tool"), 

463 "username": username, 

464 "user_password": kwargs.get("user_password"), 

465 "settings_snapshot": kwargs.get("settings_snapshot") or {}, 

466 } 

467 set_search_context(search_context) 

468 

469 system = None 

470 try: 

471 system = _init_search_system( 

472 retrievers=retrievers, llms=llms, username=username, **kwargs 

473 ) 

474 # Set progress callback if provided 

475 if progress_callback: 

476 system.set_progress_callback(progress_callback) 

477 

478 # Perform the initial research 

479 initial_findings = system.analyze_topic(query) 

480 

481 # Generate the structured report 

482 report_generator = IntegratedReportGenerator( 

483 search_system=system, 

484 llm=system.model, 

485 searches_per_section=searches_per_section, 

486 settings_snapshot=kwargs.get("settings_snapshot"), 

487 ) 

488 report = report_generator.generate_report(initial_findings, query) 

489 

490 # Save report to file if path is provided 

491 if output_file and report and "content" in report: 

492 from ..security.file_write_verifier import write_file_verified 

493 

494 write_file_verified( 

495 output_file, 

496 report["content"], 

497 "api.allow_file_output", 

498 context="API research report", 

499 settings_snapshot=kwargs.get("settings_snapshot"), 

500 ) 

501 logger.info(f"Report saved to {output_file}") 

502 report["file_path"] = output_file 

503 return report 

504 finally: 

505 if system is not None: 505 ↛ 507line 505 didn't jump to line 507 because the condition on line 505 was always true

506 _close_system(system) 

507 clear_search_context() 

508 

509 

510@no_db_settings 

511def detailed_research( 

512 query: str, 

513 research_id: str | None = None, 

514 retrievers: dict[str, Any] | None = None, 

515 llms: dict[str, Any] | None = None, 

516 username: str | None = None, 

517 **kwargs: Any, 

518) -> dict[str, Any]: 

519 """ 

520 Perform detailed research with comprehensive analysis. 

521 

522 Similar to generate_report but returns structured data instead of markdown. 

523 

524 Args: 

525 query: The research query to analyze 

526 research_id: Optional research ID (int or UUID string) for tracking metrics 

527 retrievers: Optional dictionary of {name: retriever} pairs to use as search engines 

528 llms: Optional dictionary of {name: llm} pairs to use as language models 

529 username: Optional username for per-user cache isolation 

530 **kwargs: Configuration for the search system. Pass settings_snapshot 

531 (via create_settings_snapshot()) to configure provider, temperature, etc. 

532 

533 Returns: 

534 Dictionary containing detailed research results 

535 """ 

536 logger.info("Performing detailed research for query: {}", query) 

537 

538 if "settings_snapshot" not in kwargs: 

539 logger.warning( 

540 "No settings_snapshot provided to detailed_research(). " 

541 "Using defaults and environment variables. For explicit control, " 

542 "pass settings_snapshot=create_settings_snapshot(provider=..., " 

543 "overrides={'search.tool': ...})." 

544 ) 

545 kwargs["settings_snapshot"] = create_settings_snapshot() 

546 

547 # Generate a research_id if none provided 

548 if research_id is None: 

549 import uuid 

550 

551 research_id = str(uuid.uuid4()) 

552 logger.debug(f"Generated research_id: {research_id}") 

553 

554 # Register retrievers if provided 

555 if retrievers: 

556 from ..web_search_engines.retriever_registry import retriever_registry 

557 

558 retriever_registry.register_multiple(retrievers) 

559 logger.info( 

560 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}" 

561 ) 

562 

563 # Register LLMs if provided 

564 if llms: 

565 from ..llm import register_llm 

566 

567 for name, llm_instance in llms.items(): 

568 register_llm(name, llm_instance) 

569 logger.info(f"Registered {len(llms)} LLMs: {list(llms.keys())}") 

570 

571 search_context = { 

572 "research_id": research_id, 

573 "research_query": query, 

574 "research_mode": "detailed", 

575 "research_phase": "init", 

576 "search_iteration": 0, 

577 "search_engine_selected": kwargs.get("search_tool"), 

578 "username": username, 

579 "user_password": kwargs.get("user_password"), 

580 "settings_snapshot": kwargs.get("settings_snapshot") or {}, 

581 } 

582 set_search_context(search_context) 

583 

584 system = None 

585 try: 

586 # Initialize system 

587 system = _init_search_system( 

588 retrievers=retrievers, llms=llms, username=username, **kwargs 

589 ) 

590 

591 # Perform detailed research 

592 results = system.analyze_topic(query) 

593 

594 # Return comprehensive results (guard against None results) 

595 if results is None: 595 ↛ 596line 595 didn't jump to line 596 because the condition on line 595 was never true

596 results = {} 

597 return { 

598 "query": query, 

599 "research_id": research_id, 

600 "summary": results.get("current_knowledge", ""), 

601 "findings": results.get("findings", []), 

602 "iterations": results.get("iterations", 0), 

603 "questions": results.get("questions", {}), 

604 "formatted_findings": results.get("formatted_findings", ""), 

605 "sources": results.get("all_links_of_system", []), 

606 "metadata": { 

607 "timestamp": datetime.now(UTC).isoformat(), 

608 "search_tool": kwargs.get("search_tool", "searxng"), 

609 "iterations_requested": kwargs.get("iterations", 1), 

610 "strategy": kwargs.get("search_strategy", "source_based"), 

611 }, 

612 } 

613 finally: 

614 if system is not None: 614 ↛ 616line 614 didn't jump to line 616 because the condition on line 614 was always true

615 _close_system(system) 

616 clear_search_context() 

617 

618 

619@no_db_settings 

620def analyze_documents( 

621 query: str, 

622 collection_name: str, 

623 max_results: int = 10, 

624 temperature: float = 0.7, 

625 force_reindex: bool = False, 

626 output_file: str | None = None, 

627 *, 

628 username: str | None = None, 

629 settings_snapshot: dict[str, Any] | None = None, 

630 programmatic_mode: bool = True, 

631) -> dict[str, Any]: 

632 """ 

633 Search and analyze documents in a specific local collection. 

634 

635 Args: 

636 query: The search query 

637 collection_name: Name of the local document collection to search 

638 max_results: Maximum number of results to return 

639 temperature: LLM temperature for summary generation 

640 force_reindex: Whether to force reindexing the collection 

641 output_file: Optional path to save analysis results to a file 

642 username: Optional username for thread context. REST callers pass the 

643 authenticated user; programmatic SDK callers can omit. 

644 settings_snapshot: Settings snapshot for the user's stored configuration 

645 (LLM provider/model, embedding model, etc.). REST callers pass the 

646 snapshot from the user's encrypted DB; SDK callers can omit to use 

647 JSON defaults + LDR_* env vars. 

648 programmatic_mode: If True (default for SDK callers), disables DB-backed 

649 metrics. REST callers pass False so per-user rate-limit estimates 

650 persist across requests. 

651 

652 Returns: 

653 Dictionary containing: 

654 - 'summary': Summary of the findings 

655 - 'documents': List of matching documents with content and metadata 

656 """ 

657 if settings_snapshot is None: 

658 settings_snapshot = create_settings_snapshot() 

659 

660 logger.info( 

661 f"Analyzing documents in collection '{collection_name}' for query: {query}" 

662 ) 

663 

664 llm = None 

665 search = None 

666 try: 

667 # Get language model with custom temperature 

668 llm = get_llm( 

669 temperature=temperature, settings_snapshot=settings_snapshot 

670 ) 

671 

672 # Get search engine for the specified collection 

673 search = get_search( 

674 collection_name, 

675 llm_instance=llm, 

676 username=username, 

677 settings_snapshot=settings_snapshot, 

678 programmatic_mode=programmatic_mode, 

679 ) 

680 

681 if not search: 

682 from ..utilities.resource_utils import safe_close 

683 

684 safe_close(llm, "LLM") 

685 llm = None 

686 return { 

687 "summary": f"Error: Collection '{collection_name}' not found or not properly configured.", 

688 "documents": [], 

689 } 

690 

691 # Set max results 

692 search.max_results = max_results 

693 # Perform the search 

694 results = search.run(query) 

695 

696 if not results: 

697 return { 

698 "summary": f"No documents found in collection '{collection_name}' for query: '{query}'", 

699 "documents": [], 

700 } 

701 

702 # Get LLM to generate a summary of the results 

703 

704 docs_text = "\n\n".join( 

705 [ 

706 f"Document {i + 1}:" 

707 f" {doc.get('content', doc.get('snippet', ''))[:1000]}" 

708 for i, doc in enumerate(results[:5]) 

709 ] 

710 ) # Limit to first 5 docs and 1000 chars each 

711 

712 summary_prompt = f"""Analyze these document excerpts related to the query: "{query}" 

713 

714 {docs_text} 

715 

716 Provide a concise summary of the key information found in these documents related to the query. 

717 """ 

718 

719 import time 

720 

721 llm_start_time = time.time() 

722 logger.info( 

723 f"Starting LLM summary generation (prompt length: {len(summary_prompt)} chars)..." 

724 ) 

725 

726 summary_response = llm.invoke(summary_prompt) 

727 

728 llm_elapsed = time.time() - llm_start_time 

729 logger.info(f"LLM summary generation completed in {llm_elapsed:.2f}s") 

730 

731 if hasattr(summary_response, "content"): 731 ↛ 734line 731 didn't jump to line 734 because the condition on line 731 was always true

732 summary = summary_response.content 

733 else: 

734 summary = str(summary_response) 

735 

736 # Create result dictionary 

737 analysis_result = { 

738 "summary": summary, 

739 "documents": results, 

740 "collection": collection_name, 

741 "document_count": len(results), 

742 } 

743 

744 # Save to file if requested 

745 if output_file: 

746 from ..security.file_write_verifier import write_file_verified 

747 

748 content = f"# Document Analysis: {query}\n\n" 

749 content += f"## Summary\n\n{summary}\n\n" 

750 content += f"## Documents Found: {len(results)}\n\n" 

751 

752 for i, doc in enumerate(results): 

753 content += ( 

754 f"### Document {i + 1}: {doc.get('title', 'Untitled')}\n\n" 

755 ) 

756 content += f"**Source:** {doc.get('link', 'Unknown')}\n\n" 

757 content += f"**Content:**\n\n{doc.get('content', doc.get('snippet', 'No content available'))[:1000]}...\n\n" 

758 content += "---\n\n" 

759 

760 write_file_verified( 

761 output_file, 

762 content, 

763 "api.allow_file_output", 

764 context="API document analysis", 

765 settings_snapshot=settings_snapshot, 

766 ) 

767 

768 analysis_result["file_path"] = output_file 

769 logger.info(f"Analysis saved to {output_file}") 

770 

771 return analysis_result 

772 finally: 

773 from ..utilities.resource_utils import safe_close 

774 

775 safe_close(search, "search engine") 

776 safe_close(llm, "LLM")