Service Local Deep Researcher - thekaveh/atlas GitHub Wiki
LangGraph-based multi-step research agent. The user submits a topic. LDR runs a search-summarize-reflect loop (default 3 iterations) and returns a Markdown report that cites its sources. Upstream is langchain-ai/local-deep-researcher. The stack runs the LangGraph dev server (langgraph dev) on container port 2024, published at LOCAL_DEEP_RESEARCHER_PORT.
LDR is local by design: it uses the stack's LiteLLM gateway (so any local Ollama model LiteLLM serves works) and SearXNG for web search. The default loop needs no outbound API key. The backend exposes a typed /research/start|status|result|cancel|logs|sessions|health surface. Its client calls the stock LangGraph dev-server API (/ok, /threads, /threads/{id}/runs/stream). The LDR endpoint is reachable directly and through Kong's research.localhost alias.
Image: python:3.11.15-slim. At each start, the entrypoint checks out the manifest-pinned upstream commit into the repo volume, verifies it and installs it. A restart never pulls a mutable branch. Sources: container or disabled. There is no GPU path: LDR runs no inference and orchestrates LiteLLM.
| Path | URL | Notes |
|---|---|---|
| Direct |
http://localhost:${LOCAL_DEEP_RESEARCHER_PORT} (default 63097) |
LangGraph dev-server REST API. |
| Kong | http://research.localhost:63000 |
Route generated from LOCAL_DEEP_RESEARCHER_SOURCE (needs the --setup-hosts entries). |
| LangGraph API |
POST /threads, POST /threads/{id}/runs/stream
|
Standard LangGraph dev-server endpoints. |
Browser access is limited to the Kong origin (CORS_ALLOW_ORIGINS=http://research.localhost:${KONG_HTTP_PORT}), so other web pages cannot read or delete threads through the direct port. The Backend calls the API server-side and is unaffected.
Canonical port table: Ports and Routes.
LOCAL_DEEP_RESEARCHER_SOURCE=container # container | disabled
LOCAL_DEEP_RESEARCHER_PORT=63097 # computed by topology.py
LOCAL_DEEP_RESEARCHER_REF=38f769f84380f2065de76021ac7c5215f88aa39e
LOCAL_DEEP_RESEARCHER_LANGGRAPH_CLI_VERSION=0.4.31
LOCAL_DEEP_RESEARCHER_UPSTREAM_LOCK_SHA256=26fc35ac377836de6628e5f7b180944c4d4bd50a5e9f0200bd6e663f20e35c1a
LOCAL_DEEP_RESEARCHER_LOOPS=3 # max research iterations
LOCAL_DEEP_RESEARCHER_SEARCH_API=searxng # only searxng is wired; tavily/perplexity need upstream API keys
LOCAL_DEEP_RESEARCHER_WORKERS=3 # reserved; the entrypoint does not pass --n-workers to langgraph dev
LOCAL_DEEP_RESEARCHER_FULL_PAGE_MODE=disabled # disabled | builtin | crawl4ai (crawl4ai needs CRAWL4AI_SOURCE=container); sets FETCH_FULL_PAGEAdaptive env (auto-injected):
LITELLM_BASE_URL=http://litellm:4000
LITELLM_API_KEY=${LITELLM_MASTER_KEY}
LITELLM_DEFAULT_MODEL=ollama/qwen3.8:latest # the one model every step uses
# STT_ENDPOINT / TTS_ENDPOINT / DOCLING_ENDPOINT are not injected: the LDR
# research path is text-only.Required dependencies (depends_on.required): searxng (search) and litellm (summaries). LDR connects to no database. The backend (research_service.py) calls this server over HTTP and stores research sessions in public.research_*; the Supabase dependency belongs to the backend.
Full-page mode. LOCAL_DEEP_RESEARCHER_FULL_PAGE_MODE sets FETCH_FULL_PAGE. disabled keeps search snippets only. builtin uses upstream's own page fetch. crawl4ai sends each URL to Crawl4AI; startup fails if CRAWL4AI_SOURCE is not container.
Pinned source. LOCAL_DEEP_RESEARCHER_REF must be a full commit SHA. Startup checks the upstream uv.lock against LOCAL_DEEP_RESEARCHER_UPSTREAM_LOCK_SHA256. It then installs from Atlas's hashed lock build/config/runtime-requirements.lock. A mismatch stops startup before the source tree is replaced.
To upgrade:
- Change the three pins in the manifest.
- Run
uv run --project bootstrapper python scripts/refresh-local-deep-researcher-lock.py. - Run the same command with
--check. - Commit the regenerated files under
locks/andbuild/config/. - Re-test the LDR patches and the backend research contract.
Optional adaptive (runtime_deps.local-deep-researcher.optional): neo4j-graph-db, n8n, weaviate and the media providers. The manifest declares them, but no LDR code uses them yet.
Research loop:
- Client calls
POST /threadsto create a thread. - Client calls
POST /threads/{id}/runs/streamwith the research topic. - LangGraph executes the StateGraph defined upstream:
-
generate_queryโ LiteLLM produces a search query. -
web_researchโ calls SearXNG athttp://searxng:8080/search?format=json. -
summarize_sourcesโ LiteLLM summarizes the gathered snippets into arunning_summary. -
reflect_on_summaryโ LiteLLM proposes a follow-up query; routing then checks the requested loop limit. -
finalize_summaryโ outputs the final Markdown report.
-
- State (
running_summary,sources_gathered,research_loop_count) lives in the development runtime's memory-backed checkpointer.
Checkpointer caveat. The development runtime caches checkpoints under /app/.langgraph_api in the container layer. Atlas does not mount this cache; container recreation loses it. A crash can lose changes that have not been flushed. This cache is not a supported backup or run-resumption mechanism. A durable checkpointer remains a future integration (ยง9.4).
Search backend. LOCAL_DEEP_RESEARCHER_SEARCH_API=searxng calls http://searxng:8080/search?q=โฆ&format=json. SearXNG must have formats: [json] enabled (it does, in services/searxng/config/settings.yml).
LLM gateway. Every LLM step calls LiteLLM at http://litellm:4000/v1. All steps use one model: LITELLM_DEFAULT_MODEL. Atlas's build/scripts/init-config.py reads it at startup and exits if it is empty.
Backend integration. services/backend/app/app/research_client.py calls http://local-deep-researcher:2024 with a ResearchRequest/ResearchResult schema behind the backend's /research/* routes. Sessions persist to public.research_sessions. The client checks /ok and creates a thread with POST /threads. A backend background task then calls POST /threads/{thread_id}/runs/stream with assistant_id=ollama_deep_researcher, on_disconnect=cancel and stream_mode=["values"].
Support tier: experimental โ Capability contract declared; no cited cold-start, workflow, or upgrade qualification run yet (evidence at v0.1.0).
| Capability | Status | Verification | Notes |
|---|---|---|---|
| Multi-step local research loop | supported | tested | Atlas pins the upstream LangGraph source and serving dependencies, then routes search through SearXNG and generation through LiteLLM without requiring a direct cloud key. |
| Backend-managed research sessions | partial | tested | The authenticated Backend adds bounded concurrency, cancellation, logs, and Postgres session records around LDR, while direct LangGraph callers bypass that lifecycle. |
| Research endpoint authentication | not-supported | tested | The host-published LangGraph API and CORS-only research.localhost route have no Atlas authentication; use the authenticated Backend research API or restrict direct ingress. Browser access is limited to the research.localhost origin (CORS_ALLOW_ORIGINS), so a web page cannot read threads through the direct port. |
| Research thread persistence | partial | tested | Backend session metadata persists in Postgres. LangGraph caches thread state in the container layer; recreation discards it. This cache is not durable shared storage, and retained thread state can grow without a bound. |
| Full-page Crawl4AI extraction | partial | tested | The opt-in crawl4ai mode is patched and source-gated, while disabled or builtin modes provide different extraction depth and no live web compatibility guarantee. |
| Declared optional service integrations | stubbed | documented | Neo4j, n8n, Weaviate, speech, and document services appear as runtime hints, but the LDR research code consumes only LiteLLM, SearXNG, and optional Crawl4AI today. |
| Parallel research worker control | stubbed | documented | LOCAL_DEEP_RESEARCHER_WORKERS is reserved because the entrypoint does not pass it to langgraph dev; one server process owns the in-memory research state. |
Container keeps restarting. The pinned checkout, lock verification or install failed. docker logs <project>-local-deep-researcher shows the failing step; network or PyPI access is the usual cause.
Research returns empty / "no sources gathered". SearXNG returned no JSON results. Check curl "http://localhost:${SEARXNG_PORT}/search?q=test&format=json"; if the JSON format is disabled, fix services/searxng/config/settings.yml.
Runs hang at summarize_sources. LiteLLM is unreachable or the model is overloaded. Run docker logs <project>-litellm -f and confirm that LiteLLM serves LITELLM_DEFAULT_MODEL.
State lost after container recreation. Atlas does not mount the development cache at /app/.langgraph_api. Recreating the container discards it (ยง4). A restart can reload flushed state in the same container, but this is not a supported recovery procedure.
Kong route 404 for research.localhost. Kong has the route only when LOCAL_DEEP_RESEARCHER_SOURCE=container. A Kong 404 ("no Route matched") means the service is disabled; set the source and rerun ./start.sh. If the host name does not resolve at all, run ./start.sh --setup-hosts.
docker compose ps local-deep-researcher
docker compose logs -f local-deep-researcher
curl -s http://localhost:${LOCAL_DEEP_RESEARCHER_PORT}/threads -X POST -H 'content-type: application/json' -d '{}'For general startup and routing issues, see Troubleshooting.
Run a research session from the CLI.
# 1. Create a thread
THREAD=$(curl -s -X POST http://localhost:${LOCAL_DEEP_RESEARCHER_PORT}/threads \
-H 'content-type: application/json' -d '{}' | jq -r .thread_id)
# 2. Stream a run (SSE)
curl -N -X POST http://localhost:${LOCAL_DEEP_RESEARCHER_PORT}/threads/$THREAD/runs/stream \
-H 'content-type: application/json' \
-d '{"assistant_id":"ollama_deep_researcher","on_disconnect":"cancel","input":{"research_topic":"vector database trade-offs 2026"}}'Read the last values event's running_summary field for the Markdown report.
Inspect thread state.
curl -s http://localhost:${LOCAL_DEEP_RESEARCHER_PORT}/threads/$THREAD/state | jq '.values'The .values object contains running_summary, sources_gathered and research_loop_count. Scheduled nodes are in the full response's .next field.
Tune the research depth. LOCAL_DEEP_RESEARCHER_LOOPS (default 3) and LOCAL_DEEP_RESEARCHER_SEARCH_API are defaults. A run's own max_web_research_loops and search_api override them. Backend /research/start fills omitted values from these variables. The Backend waits up to max(300, 180 * max_loops) seconds for the run stream. All other settings (provider, model, LiteLLM URL and key) come only from the environment, so a caller cannot redirect the LiteLLM key.
Change the model. Set LITELLM_DEFAULT_MODEL in .env to a model LiteLLM serves, then restart the stack.
- Cost per run. About 2 LLM calls per loop plus 1 (about 7 for the default 3 loops), and one SearXNG call per loop. Loop time depends on the model and hardware. Local Ollama models have no per-call cost; cloud models through LiteLLM are metered.
-
No streaming to clients. The backend's
research_client.pyreads the/runs/streamSSE channel asynchronously and does not forward events. - Thread state. Each thread keeps its summary and sources in the memory-backed development checkpointer. After completed result handling, the Backend attempts thread deletion. Direct callers must manage their own threads; threads from incomplete runs remain.
| Service | Category |
|---|---|
| litellm | llm |
| crawl4ai | media |
| searxng | media |
| Service | Category |
|---|---|
| kong | infra |
| backend | apps |
| open-webui | apps |

Open the full-size diagram for a full-screen view.
-
local-deep-researcher โ redis โ Why: The development cache stays in the container layer; recreation loses it. No durable shared checkpointer is wired today. Mechanism: swap checkpointer to
langgraph.checkpoint.redis.RedisSaverpointed at a fresh index, such asredis://:${REDIS_PASSWORD}@redis:6379/6(see Redis database-index convention); addREDIS_URLto LDR env. Effort: small. Confidence: medium. -
local-deep-researcher โ neo4j โ Why: each research run yields
sources_gathered+ arunning_summary. Writing these as(Topic)-[CITES]->(Source)triples lets later runs detect overlap and reuse evidence. Mechanism: post-finalize_summarycallback writes CypherMERGEviabolt://neo4j-graph-db:7687. Effort: medium. Confidence: medium. -
local-deep-researcher โ minio โ Why: the final markdown report lives only in LangGraph thread state and the Backend's
public.research_*rows; no object store holds it. Mechanism: onfinalize_summary, S3PutObjectto${MINIO_ENDPOINT}bucketresearch-reportskeyed bysession_id. Effort: small. Confidence: medium. -
local-deep-researcher โ hermes โ Why: Hermes has no path to invoke multi-step web research today. Exposing LDR as a Hermes tool turns "deep research" into a single tool call. Mechanism: a Hermes skill (
/opt/data/skills/<category>/<name>/SKILL.md, installed by hermes-init) or an MCP server that POSTs tohttp://local-deep-researcher:2024/threads/{id}/runs/streamand returns the final summary. Effort: medium. Confidence: medium.
-
open_deep_research (langchain-ai) โ Headline: multi-agent deep-research engine (supervisor + parallel sub-researchers) evaluated as a disabled-by-default opt-in second research engine complementing LDR (LDR stays the fast/local/key-free tier). GO-conditional per
docs/strategy/langchain-stack-evaluation.md, gated on a key-free LiteLLM+SearXNG boot at acceptable cost; itsmessages/final_reportschema needs a per-engine branch in the backend research client. -
Firecrawl (details) โ Headline: self-hosted JS-rendering scraper that returns clean markdown, replacing LDR's
builtin/crawl4aifull-page fetch with structured extraction. Wires into: n8n, backend, hermes.
- Persistent LangGraph checkpointer โ Why pursue: the development cache does not survive container recreation or provide supported durable run recovery. Effort: small.
-
Tavily / Perplexity search backends โ Why pursue: upstream supports both via
SEARCH_API=tavily|perplexityplus API keys; Atlas injects no key for either. Effort: small. -
USE_TOOL_CALLINGfor gpt-oss models โ Why pursue: enables structured tool calls instead of JSON mode for gpt-oss family, improving reliability with LiteLLM-routed local models. Effort: small. -
STRIP_THINKING_TOKENStoggle โ Why pursue: Hermes-style reasoning models leak<think>blocks into the report; upstream env var hides them. Effort: small. -
LangSmith tracing โ Why pursue:
LANGSMITH_API_KEYships upstream; superseded if Langfuse lands but useful as a stopgap. Effort: small.