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
« 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"""
6from datetime import datetime, UTC
7from typing import Any, Callable
9from loguru import logger
10from local_deep_research.settings.logger import log_settings
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
22def _close_system(system):
23 """Close an AdvancedSearchSystem and its associated resources."""
24 from ..utilities.resource_utils import safe_close
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")
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.
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
76 Returns:
77 AdvancedSearchSystem: An instance of the configured AdvancedSearchSystem.
79 """
80 # Register retrievers if provided
81 if retrievers:
82 from ..web_search_engines.retriever_registry import retriever_registry
84 retriever_registry.register_multiple(retrievers, username=username)
85 logger.info(
86 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}"
87 )
89 # Register LLMs if provided
90 if llms:
91 from ..llm import register_llm
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())}")
97 # Use settings_snapshot from parameter, or fall back to kwargs
98 if settings_snapshot is None:
99 settings_snapshot = kwargs.get("settings_snapshot")
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 )
113 # Set the search engine if specified or get from settings
114 search_engine = None
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 )
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 )
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
152 safe_close(llm, "init LLM")
153 raise
155 # Override default settings with user-provided values
156 system.max_iterations = iterations
157 system.questions_per_iteration = questions_per_iteration
159 # Set progress callback if provided
160 if progress_callback:
161 system.set_progress_callback(progress_callback)
163 return system
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.
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()`.
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
208 Examples:
209 # Simple usage with defaults
210 result = quick_summary("What is quantum computing?")
212 # With custom provider
213 result = quick_summary(
214 "What is quantum computing?",
215 provider="anthropic",
216 api_key="your-api-key-here"
217 )
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)
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
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 )
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 )
265 # Generate a research_id if none provided
266 if research_id is None:
267 import uuid
269 research_id = str(uuid.uuid4())
270 logger.debug(f"Generated research_id: {research_id}")
272 # Register retrievers if provided
273 if retrievers:
274 from ..web_search_engines.retriever_registry import retriever_registry
276 retriever_registry.register_multiple(retrievers, username=username)
277 logger.info(
278 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}"
279 )
281 # Register LLMs if provided
282 if llms:
283 from ..llm import register_llm
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())}")
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)
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)
317 # Perform the search and analysis
318 results = system.analyze_topic(query)
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."
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()
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.
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.
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)
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)
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)
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
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 )
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 )
438 # Register retrievers if provided
439 if retrievers:
440 from ..web_search_engines.retriever_registry import retriever_registry
442 retriever_registry.register_multiple(retrievers, username=username)
443 logger.info(
444 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}"
445 )
447 # Register LLMs if provided
448 if llms:
449 from ..llm import register_llm
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())}")
455 import uuid
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)
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)
479 # Perform the initial research
480 initial_findings = system.analyze_topic(query)
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)
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
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()
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.
523 Similar to generate_report but returns structured data instead of markdown.
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.
534 Returns:
535 Dictionary containing detailed research results
536 """
537 logger.info("Performing detailed research for query: {}", query)
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()
548 # Generate a research_id if none provided
549 if research_id is None:
550 import uuid
552 research_id = str(uuid.uuid4())
553 logger.debug(f"Generated research_id: {research_id}")
555 # Register retrievers if provided
556 if retrievers:
557 from ..web_search_engines.retriever_registry import retriever_registry
559 retriever_registry.register_multiple(retrievers, username=username)
560 logger.info(
561 f"Registered {len(retrievers)} retrievers: {list(retrievers.keys())}"
562 )
564 # Register LLMs if provided
565 if llms:
566 from ..llm import register_llm
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())}")
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)
585 system = None
586 try:
587 # Initialize system
588 system = _init_search_system(
589 retrievers=retrievers, llms=llms, username=username, **kwargs
590 )
592 # Perform detailed research
593 results = system.analyze_topic(query)
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()
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.
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.
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()
661 logger.info(
662 f"Analyzing documents in collection '{collection_name}' for query: {query}"
663 )
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 )
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 )
686 if not search:
687 from ..utilities.resource_utils import safe_close
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 }
696 # Set max results
697 search.max_results = max_results
698 # Perform the search
699 results = search.run(query)
701 if not results:
702 return {
703 "summary": f"No documents found in collection '{collection_name}' for query: '{query}'",
704 "documents": [],
705 }
707 # Get LLM to generate a summary of the results
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
717 summary_prompt = f"""Analyze these document excerpts related to the query: "{query}"
719 {docs_text}
721 Provide a concise summary of the key information found in these documents related to the query.
722 """
724 import time
726 llm_start_time = time.time()
727 logger.info(
728 f"Starting LLM summary generation (prompt length: {len(summary_prompt)} chars)..."
729 )
731 summary_response = llm.invoke(summary_prompt)
733 llm_elapsed = time.time() - llm_start_time
734 logger.info(f"LLM summary generation completed in {llm_elapsed:.2f}s")
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)
741 # Create result dictionary
742 analysis_result = {
743 "summary": summary,
744 "documents": results,
745 "collection": collection_name,
746 "document_count": len(results),
747 }
749 # Save to file if requested
750 if output_file:
751 from ..security.file_write_verifier import write_file_verified
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"
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"
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 )
773 analysis_result["file_path"] = output_file
774 logger.info(f"Analysis saved to {output_file}")
776 return analysis_result
777 finally:
778 from ..utilities.resource_utils import safe_close
780 safe_close(search, "search engine")
781 safe_close(llm, "LLM")