#!/usr/bin/env python3 """ LDR Research Script Reads a research query from stdin and uses Local Deep Research to find relevant documentation, sources, and context. Returns JSON with the research output. Usage: # Pipe a query from stdin echo "What is RAG?" | python scripts/ldr-research.py # Or pass a file python scripts/ldr-research.py < query.txt # With CLI arguments (easier local testing) python scripts/ldr-research.py --provider openrouter --model gpt-4o < query.txt Output: JSON with research results, sources, and findings. Environment variables (can be overridden by CLI args): OPENROUTER_API_KEY - API key for OpenRouter SERPER_API_KEY - API key for Serper.dev search LDR_PROVIDER - LLM provider (default: openrouter) LDR_SEARCH_TOOL - Search tool (default: serper) LDR_RESEARCH_MODEL - Model name (default: google/gemini-2.0-flash-001 for openrouter) LDR_STRATEGY - Search strategy (default: langgraph-agent) Note: This uses the programmatic API and does NOT require a running LDR server. """ import argparse import faulthandler import json import os import sys # Dump a Python traceback to stderr on SIGABRT/SIGSEGV/SIGFPE/SIGBUS/SIGILL. faulthandler.enable() def make_serializable(obj): """Convert objects to JSON-serializable format.""" if obj is None: return None if isinstance(obj, (str, int, float, bool)): return obj if isinstance(obj, dict): return {k: make_serializable(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): return [make_serializable(item) for item in obj] # Handle LangChain Document objects if hasattr(obj, "page_content") and hasattr(obj, "metadata"): return { "content": obj.page_content, "metadata": make_serializable(obj.metadata), } # Handle other objects with __dict__ if hasattr(obj, "__dict__"): return make_serializable(obj.__dict__) # Fallback to string representation return str(obj) def parse_args(): parser = argparse.ArgumentParser( description="Run LDR research on a query read from stdin" ) parser.add_argument( "--provider", default=os.environ.get("LDR_PROVIDER", "openrouter"), help="LLM provider (default: openrouter)", ) parser.add_argument( "--search-tool", default=os.environ.get("LDR_SEARCH_TOOL", "serper"), help="Search tool (default: serper)", ) parser.add_argument( "--model", default=os.environ.get("LDR_RESEARCH_MODEL"), help="Model name (default: provider's default)", ) parser.add_argument( "--iterations", type=int, default=None, help=( "Number of research iterations. If unset, the strategy uses " "its own default (e.g. langgraph-agent reads " "langgraph_agent.max_iterations from settings)." ), ) parser.add_argument( "--strategy", default=os.environ.get("LDR_STRATEGY", "langgraph-agent"), help="Search strategy name (default: langgraph-agent)", ) return parser.parse_args() def main(): # Flush in finally: SIGABRT during interpreter shutdown won't drain the stdout buffer. try: args = parse_args() # Read query from stdin query = sys.stdin.read().strip() if not query: print(json.dumps({"error": "No query provided on stdin"})) sys.exit(1) # Default model for OpenRouter if not specified model_name = args.model if not model_name and args.provider == "openrouter": model_name = "google/gemini-2.0-flash-001" # Check required API keys if args.provider == "openrouter" and not os.environ.get( "OPENROUTER_API_KEY" ): print(json.dumps({"error": "OPENROUTER_API_KEY not set"})) sys.exit(1) if args.search_tool == "serper" and not os.environ.get( "SERPER_API_KEY" ): print(json.dumps({"error": "SERPER_API_KEY not set"})) sys.exit(1) try: from local_deep_research.api import quick_summary from local_deep_research.api.settings_utils import ( create_settings_snapshot, ) # Build settings overrides overrides = { "search.tool": args.search_tool, "llm.provider": args.provider, } if model_name: overrides["llm.model"] = model_name # Add API keys from environment if os.environ.get("OPENROUTER_API_KEY"): overrides["llm.openrouter.api_key"] = os.environ[ "OPENROUTER_API_KEY" ] if os.environ.get("SERPER_API_KEY"): overrides["search.engine.web.serper.api_key"] = os.environ[ "SERPER_API_KEY" ] settings = create_settings_snapshot(overrides=overrides) # Build kwargs kwargs = { "query": query, "provider": args.provider, "search_tool": args.search_tool, "settings_snapshot": settings, "programmatic_mode": True, "search_strategy": args.strategy, } if model_name: kwargs["model_name"] = model_name if args.iterations is not None: kwargs["iterations"] = args.iterations result = quick_summary(**kwargs) # Use formatted_findings if available (already properly formatted with sources) # Fall back to summary if not research_output = result.get("formatted_findings") or result.get( "summary", str(result) ) # Build output - make sure everything is JSON serializable output = { "research": research_output, "sources": make_serializable(result.get("sources", [])), "findings": make_serializable(result.get("findings", [])), "iterations": result.get("iterations"), } print(json.dumps(output)) except Exception as e: print(json.dumps({"error": str(e)})) sys.exit(1) finally: try: sys.stdout.flush() except Exception: # noqa: silent-exception pass if __name__ == "__main__": main()