Deep Research
A FastAPI research service that decomposes a query, runs iterative web + LinkedIn searches, scores coverage gaps with Claude on Bedrock, and writes a structured Markdown report — offered as both a fixed-depth pipeline and an autonomous agentic loop.

Overview
Deep Research is a FastAPI backend that conducts comprehensive web-based research from a single query. It ships two complementary engines. The first is a deterministic multi-stage pipeline (POST /deep-research) with three depth levels — Shallow, Intermediate, Deep — that parses intent into research targets, routes each target to a person / company / generic workflow, then loops through analysis rounds where Claude (via AWS Bedrock) extracts NoteItem facts, scores coverage per purpose (FULFILLED / PARTIAL / UNFULFILLED), and generates fresh Tavily queries to close identified gaps before formatting everything into a Markdown report. The second, added on the agentic branch, is a true tool-calling agent (POST /agent): Claude Haiku 4.5 runs an autonomous loop (up to 20 iterations) with five LangChain StructuredTools bound — web_search, page_fetcher, company_profile, person_profile, and person_experience — planning its own tool calls until it has enough to synthesise a sourced answer.
Key features
Autonomous Agentic Loop
POST /agent runs Claude Haiku 4.5 in a tool-calling loop (up to 20 iterations) with five bound tools — it plans which tools to call, reads pages, cross-references LinkedIn, and synthesises a sourced answer without a fixed script.
Three-Depth Research Pipeline
POST /deep-research supports Shallow, Intermediate, and Deep levels — each adds another analysis + search round, so callers trade latency and cost for thoroughness explicitly.
Intent Decomposition
A query is parsed into a structured ResearchIntent — discrete research targets and purposes — which drives routing and gives every later stage something concrete to score against.
Coverage Gap Scoring
Each round has Claude score every purpose as FULFILLED / PARTIAL / UNFULFILLED and emit new SearchQuery objects aimed only at the gaps, turning research into measurable gap-closing.
Person / Company / Generic Workflows
Stage 0 routes each target to a specialised workflow — LinkedIn person profile + post analysis, company profile + posts, or generic Tavily web search.
Five Research Tools
web_search, page_fetcher (clean full-text extraction), company_profile and person_profile / person_experience (OrbitAIM LinkedIn data) — shared by both the agent and the pipeline.
MPNet Semantic Classification
LinkedIn posts are classified via an MPNet SentenceTransformer for keyword extraction, running on CPU-only PyTorch alongside the API.
Source-Attributed Markdown Reports
All accumulated NoteItem facts and findings are formatted into a readable Markdown report with per-fact source URLs.
Built with
Thorough research on a person, company, or topic means running many searches, reading full pages, cross-referencing LinkedIn, and tracking what's still unknown — slow, manual, and inconsistent. A single LLM call can't do it: it hallucinates, has no live data, and no notion of coverage gaps. The system needed to gather real web and LinkedIn data, reason about what's still missing, and iterate — either along a predictable depth budget or by letting the model drive its own tool use.
Automates end-to-end web research — turning a single natural-language query into a source-attributed Markdown report — through two engines: a staged multi-round research pipeline and a tool-calling agent that plans and iterates on its own.
Engineering challenges
Modelling research as iterative gap-closing rather than a single pass — each round scores coverage per purpose and generates targeted follow-up queries, so depth is a budget (Shallow/Intermediate/Deep) rather than a fixed prompt.
Building a tool-calling agent loop over Bedrock — binding five async tools as LangChain StructuredTools, matching model tool calls by name, invoking them via .ainvoke(), and capping runaway iteration at 20 rounds.
Routing each research target to the right workflow (person vs company vs generic) and, for people, chaining person_profile → person_experience using the LinkedIn URL returned by the first call.
Filtering noisy web results down to signal — keeping only Tavily hits above a 0.9 confidence threshold, matching keywords, and deduplicating already-covered topics.
Semantic classification of LinkedIn posts with an MPNet SentenceTransformer (CPU-only PyTorch) for keyword extraction, kept lightweight enough to run alongside the API.
Per-stage LLM routing — intent, shallow, intermediate, deep, markdown, and agentic stages each resolve their own model id from AWS Secrets Manager, so models can be tuned per stage without code changes.
Key decisions
Two engines behind one service — a deterministic staged pipeline for predictable cost/latency, and an autonomous agent for open-ended queries — sharing the same tools and data sources.
Claude on AWS Bedrock across all stages — reuses existing AWS auth and keeps data in-boundary, with a different model tunable per stage via Secrets Manager.
LangChain StructuredTool wrappers with a name→tool registry, so the agent loop and any direct call resolve and invoke tools uniformly (Pydantic args in, JSON out).
Coverage scoring with explicit FULFILLED / PARTIAL / UNFULFILLED states drives query generation — the pipeline stops spending search budget once purposes are fulfilled.
Confidence-threshold + keyword + dedup filtering on Tavily results to keep only high-signal sources before they reach the LLM.
All secrets (Tavily keys, model ids, LinkedIn API creds) loaded from AWS Secrets Manager (prod/orbit) at startup rather than committed env values.