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

228 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 15:42 +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, username=username) 

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, username=username) 

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 username=username, 

111 ) 

112 

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

114 search_engine = None 

115 

116 try: 

117 # If no search_tool provided, get from settings_snapshot 

118 if not search_tool and settings_snapshot: 

119 search_tool = get_setting_from_snapshot( 

120 "search.tool", settings_snapshot=settings_snapshot 

121 ) 

122 

123 if search_tool: 

124 search_engine = get_search( 

125 search_tool, 

126 llm_instance=llm, 

127 username=username, 

128 settings_snapshot=settings_snapshot, 

129 programmatic_mode=programmatic_mode, 

130 ) 

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

132 logger.warning( 

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

134 ) 

135 

136 # Create search system with custom parameters 

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

138 system = AdvancedSearchSystem( 

139 llm=llm, 

140 search=search_engine, 

141 strategy_name=search_strategy, 

142 username=username, 

143 research_id=research_id, 

144 research_context=research_context, 

145 settings_snapshot=settings_snapshot, 

146 programmatic_mode=programmatic_mode, 

147 search_original_query=search_original_query, 

148 ) 

149 except Exception: 

150 from ..utilities.resource_utils import safe_close 

151 

152 safe_close(llm, "init LLM") 

153 raise 

154 

155 # Override default settings with user-provided values 

156 system.max_iterations = iterations 

157 system.questions_per_iteration = questions_per_iteration 

158 

159 # Set progress callback if provided 

160 if progress_callback: 

161 system.set_progress_callback(progress_callback) 

162 

163 return system 

164 

165 

166@no_db_settings 

167def quick_summary( 

168 query: str, 

169 research_id: str | None = None, 

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

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

172 username: str | None = None, 

173 provider: str | None = None, 

174 api_key: str | None = None, 

175 temperature: float | None = None, 

176 max_search_results: int | None = None, 

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

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

179 search_original_query: bool = True, 

180 **kwargs: Any, 

181) -> dict[str, Any]: 

182 """ 

183 Generate a quick research summary for a given query. 

184 

185 Args: 

186 query: The research query to analyze 

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

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

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

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

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

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

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

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

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

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

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

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

199 `_init_search_system()`. 

200 

201 Returns: 

202 Dictionary containing the research results with keys: 

203 - 'summary': The generated summary text 

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

205 - 'iterations': Number of iterations performed 

206 - 'questions': Questions generated during research 

207 

208 Examples: 

209 # Simple usage with defaults 

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

211 

212 # With custom provider 

213 result = quick_summary( 

214 "What is quantum computing?", 

215 provider="anthropic", 

216 api_key="your-api-key-here" 

217 ) 

218 

219 # With advanced settings 

220 result = quick_summary( 

221 "What is quantum computing?", 

222 temperature=0.2, 

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

224 ) 

225 """ 

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

227 

228 if "settings_snapshot" not in kwargs: 

229 snapshot_kwargs = {} 

230 if provider is not None: 

231 snapshot_kwargs["provider"] = provider 

232 if api_key is not None: 

233 snapshot_kwargs["api_key"] = api_key 

234 if temperature is not None: 

235 snapshot_kwargs["temperature"] = temperature 

236 if max_search_results is not None: 

237 snapshot_kwargs["max_search_results"] = max_search_results 

238 

239 if ( 

240 not snapshot_kwargs 

241 and settings is None 

242 and settings_override is None 

243 ): 

244 logger.warning( 

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

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

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

248 ) 

249 

250 kwargs["settings_snapshot"] = create_settings_snapshot( 

251 base_settings=settings, 

252 overrides=settings_override, 

253 **snapshot_kwargs, 

254 ) 

255 log_settings( 

256 kwargs["settings_snapshot"], 

257 "Created settings snapshot for programmatic API", 

258 ) 

259 else: 

260 log_settings( 

261 kwargs["settings_snapshot"], 

262 "Using provided settings snapshot for programmatic API", 

263 ) 

264 

265 # Generate a research_id if none provided 

266 if research_id is None: 

267 import uuid 

268 

269 research_id = str(uuid.uuid4()) 

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

271 

272 # Register retrievers if provided 

273 if retrievers: 

274 from ..web_search_engines.retriever_registry import retriever_registry 

275 

276 retriever_registry.register_multiple(retrievers, username=username) 

277 logger.info( 

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

279 ) 

280 

281 # Register LLMs if provided 

282 if llms: 

283 from ..llm import register_llm 

284 

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

286 register_llm(name, llm_instance, username=username) 

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

288 

289 search_context = { 

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

291 "research_query": query, 

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

293 "research_phase": "init", 

294 "search_iteration": 0, 

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

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

297 "user_password": kwargs.get( 

298 "user_password" 

299 ), # Include password for metrics tracking 

300 # Thread-safe settings snapshot propagated to background search 

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

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

303 } 

304 set_search_context(search_context) 

305 

306 system = None 

307 try: 

308 # Remove research_mode from kwargs before passing to _init_search_system 

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

310 # Make sure username is passed to the system 

311 init_kwargs["username"] = username 

312 init_kwargs["research_id"] = research_id 

313 init_kwargs["research_context"] = search_context 

314 init_kwargs["search_original_query"] = search_original_query 

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

316 

317 # Perform the search and analysis 

318 results = system.analyze_topic(query) 

319 

320 # Extract the summary from the current knowledge 

321 if results and "current_knowledge" in results: 

322 summary = results["current_knowledge"] 

323 else: 

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

325 

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

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

328 results = {} 

329 return { 

330 "research_id": research_id, 

331 "summary": summary, 

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

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

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

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

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

337 } 

338 finally: 

339 if system is not None: 

340 _close_system(system) 

341 clear_search_context() 

342 

343 

344@no_db_settings 

345def generate_report( 

346 query: str, 

347 output_file: str | None = None, 

348 progress_callback: Callable | None = None, 

349 searches_per_section: int = 2, 

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

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

352 username: str | None = None, 

353 provider: str | None = None, 

354 api_key: str | None = None, 

355 temperature: float | None = None, 

356 max_search_results: int | None = None, 

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

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

359 **kwargs: Any, 

360) -> dict[str, Any]: 

361 """ 

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

363 

364 Args: 

365 query: The research query to analyze 

366 output_file: Optional path to save report markdown file 

367 progress_callback: Optional callback function to receive progress updates 

368 searches_per_section: The number of searches to perform for each 

369 section in the report. 

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

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

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

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

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

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

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

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

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

379 

380 Returns: 

381 Dictionary containing the research report with keys: 

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

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

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

385 

386 Examples: 

387 # Simple usage with settings snapshot 

388 from local_deep_research.api.settings_utils import create_settings_snapshot 

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

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

391 

392 # Save to file 

393 result = generate_report( 

394 "AI research", 

395 output_file="report.md", 

396 settings_snapshot=settings 

397 ) 

398 """ 

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

400 

401 if "settings_snapshot" not in kwargs: 

402 snapshot_kwargs = {} 

403 if provider is not None: 

404 snapshot_kwargs["provider"] = provider 

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

406 snapshot_kwargs["api_key"] = api_key 

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

408 snapshot_kwargs["temperature"] = temperature 

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

410 snapshot_kwargs["max_search_results"] = max_search_results 

411 

412 if ( 

413 not snapshot_kwargs 

414 and settings is None 

415 and settings_override is None 

416 ): 

417 logger.warning( 

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

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

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

421 ) 

422 

423 kwargs["settings_snapshot"] = create_settings_snapshot( 

424 base_settings=settings, 

425 overrides=settings_override, 

426 **snapshot_kwargs, 

427 ) 

428 log_settings( 

429 kwargs["settings_snapshot"], 

430 "Created settings snapshot for programmatic API", 

431 ) 

432 else: 

433 log_settings( 

434 kwargs["settings_snapshot"], 

435 "Using provided settings snapshot for programmatic API", 

436 ) 

437 

438 # Register retrievers if provided 

439 if retrievers: 

440 from ..web_search_engines.retriever_registry import retriever_registry 

441 

442 retriever_registry.register_multiple(retrievers, username=username) 

443 logger.info( 

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

445 ) 

446 

447 # Register LLMs if provided 

448 if llms: 

449 from ..llm import register_llm 

450 

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

452 register_llm(name, llm_instance, username=username) 

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

454 

455 import uuid 

456 

457 search_context = { 

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

459 "research_query": query, 

460 "research_mode": "report", 

461 "research_phase": "init", 

462 "search_iteration": 0, 

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

464 "username": username, 

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

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

467 } 

468 set_search_context(search_context) 

469 

470 system = None 

471 try: 

472 system = _init_search_system( 

473 retrievers=retrievers, llms=llms, username=username, **kwargs 

474 ) 

475 # Set progress callback if provided 

476 if progress_callback: 

477 system.set_progress_callback(progress_callback) 

478 

479 # Perform the initial research 

480 initial_findings = system.analyze_topic(query) 

481 

482 # Generate the structured report 

483 report_generator = IntegratedReportGenerator( 

484 search_system=system, 

485 llm=system.model, 

486 searches_per_section=searches_per_section, 

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

488 ) 

489 report = report_generator.generate_report(initial_findings, query) 

490 

491 # Save report to file if path is provided 

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

493 from ..security.file_write_verifier import write_file_verified 

494 

495 write_file_verified( 

496 output_file, 

497 report["content"], 

498 "api.allow_file_output", 

499 context="API research report", 

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

501 ) 

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

503 report["file_path"] = output_file 

504 return report 

505 finally: 

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

507 _close_system(system) 

508 clear_search_context() 

509 

510 

511@no_db_settings 

512def detailed_research( 

513 query: str, 

514 research_id: str | None = None, 

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

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

517 username: str | None = None, 

518 **kwargs: Any, 

519) -> dict[str, Any]: 

520 """ 

521 Perform detailed research with comprehensive analysis. 

522 

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

524 

525 Args: 

526 query: The research query to analyze 

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

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

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

530 username: Optional username for per-user cache isolation 

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

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

533 

534 Returns: 

535 Dictionary containing detailed research results 

536 """ 

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

538 

539 if "settings_snapshot" not in kwargs: 

540 logger.warning( 

541 "No settings_snapshot provided to detailed_research(). " 

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

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

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

545 ) 

546 kwargs["settings_snapshot"] = create_settings_snapshot() 

547 

548 # Generate a research_id if none provided 

549 if research_id is None: 

550 import uuid 

551 

552 research_id = str(uuid.uuid4()) 

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

554 

555 # Register retrievers if provided 

556 if retrievers: 

557 from ..web_search_engines.retriever_registry import retriever_registry 

558 

559 retriever_registry.register_multiple(retrievers, username=username) 

560 logger.info( 

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

562 ) 

563 

564 # Register LLMs if provided 

565 if llms: 

566 from ..llm import register_llm 

567 

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

569 register_llm(name, llm_instance, username=username) 

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

571 

572 search_context = { 

573 "research_id": research_id, 

574 "research_query": query, 

575 "research_mode": "detailed", 

576 "research_phase": "init", 

577 "search_iteration": 0, 

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

579 "username": username, 

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

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

582 } 

583 set_search_context(search_context) 

584 

585 system = None 

586 try: 

587 # Initialize system 

588 system = _init_search_system( 

589 retrievers=retrievers, llms=llms, username=username, **kwargs 

590 ) 

591 

592 # Perform detailed research 

593 results = system.analyze_topic(query) 

594 

595 # Return comprehensive results (guard against None results) 

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

597 results = {} 

598 return { 

599 "query": query, 

600 "research_id": research_id, 

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

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

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

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

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

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

607 "metadata": { 

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

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

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

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

612 }, 

613 } 

614 finally: 

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

616 _close_system(system) 

617 clear_search_context() 

618 

619 

620@no_db_settings 

621def analyze_documents( 

622 query: str, 

623 collection_name: str, 

624 max_results: int = 10, 

625 temperature: float = 0.7, 

626 force_reindex: bool = False, 

627 output_file: str | None = None, 

628 *, 

629 username: str | None = None, 

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

631 programmatic_mode: bool = True, 

632) -> dict[str, Any]: 

633 """ 

634 Search and analyze documents in a specific local collection. 

635 

636 Args: 

637 query: The search query 

638 collection_name: Name of the local document collection to search 

639 max_results: Maximum number of results to return 

640 temperature: LLM temperature for summary generation 

641 force_reindex: Whether to force reindexing the collection 

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

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

644 authenticated user; programmatic SDK callers can omit. 

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

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

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

648 JSON defaults + LDR_* env vars. 

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

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

651 persist across requests. 

652 

653 Returns: 

654 Dictionary containing: 

655 - 'summary': Summary of the findings 

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

657 """ 

658 if settings_snapshot is None: 

659 settings_snapshot = create_settings_snapshot() 

660 

661 logger.info( 

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

663 ) 

664 

665 llm = None 

666 search = None 

667 try: 

668 # Get language model with custom temperature. Pass username so a 

669 # per-user registered LLM (and per-user egress classification) 

670 # resolves for this caller. 

671 llm = get_llm( 

672 temperature=temperature, 

673 settings_snapshot=settings_snapshot, 

674 username=username, 

675 ) 

676 

677 # Get search engine for the specified collection 

678 search = get_search( 

679 collection_name, 

680 llm_instance=llm, 

681 username=username, 

682 settings_snapshot=settings_snapshot, 

683 programmatic_mode=programmatic_mode, 

684 ) 

685 

686 if not search: 

687 from ..utilities.resource_utils import safe_close 

688 

689 safe_close(llm, "LLM") 

690 llm = None 

691 return { 

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

693 "documents": [], 

694 } 

695 

696 # Set max results 

697 search.max_results = max_results 

698 # Perform the search 

699 results = search.run(query) 

700 

701 if not results: 

702 return { 

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

704 "documents": [], 

705 } 

706 

707 # Get LLM to generate a summary of the results 

708 

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

710 [ 

711 f"Document {i + 1}:" 

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

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

714 ] 

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

716 

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

718 

719 {docs_text} 

720 

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

722 """ 

723 

724 import time 

725 

726 llm_start_time = time.time() 

727 logger.info( 

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

729 ) 

730 

731 summary_response = llm.invoke(summary_prompt) 

732 

733 llm_elapsed = time.time() - llm_start_time 

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

735 

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

737 summary = summary_response.content 

738 else: 

739 summary = str(summary_response) 

740 

741 # Create result dictionary 

742 analysis_result = { 

743 "summary": summary, 

744 "documents": results, 

745 "collection": collection_name, 

746 "document_count": len(results), 

747 } 

748 

749 # Save to file if requested 

750 if output_file: 

751 from ..security.file_write_verifier import write_file_verified 

752 

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

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

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

756 

757 for i, doc in enumerate(results): 

758 content += ( 

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

760 ) 

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

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

763 content += "---\n\n" 

764 

765 write_file_verified( 

766 output_file, 

767 content, 

768 "api.allow_file_output", 

769 context="API document analysis", 

770 settings_snapshot=settings_snapshot, 

771 ) 

772 

773 analysis_result["file_path"] = output_file 

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

775 

776 return analysis_result 

777 finally: 

778 from ..utilities.resource_utils import safe_close 

779 

780 safe_close(search, "search engine") 

781 safe_close(llm, "LLM")