Spy Search’s search pipeline is built on an abstractDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/JasonHonKL/spy-search/llms.txt
Use this file to discover all available pages before exploring further.
Agent framework where each agent has a clearly defined role. A central Server drives the loop, passing messages through lightweight Router wrappers until the pipeline reaches a TERMINATE signal and a final report is returned to the caller.
The Agent Abstract Base Class
Every agent in the system extends src/agent/agent.py::Agent. The class enforces three abstract methods and provides one shared utility:
run(response, data) — main async entry point
Each agent’s core logic lives here. The method receives the current task string as response and a data list that accumulates results across the pipeline. It must return a routing dict (see Data flow format below).
get_recv_format() and get_send_format()
These methods define the Pydantic BaseModel schemas that describe what data an agent expects to receive and what it promises to return. Concrete agents override them when strict schema validation is needed.
_extract_response(res) — LLM output parser
A shared utility that reliably extracts a JSON object or Python literal from raw LLM output, regardless of whether the model wrapped it in a markdown code block or emitted plain text. The algorithm:
- Searches for
```json … ```or``` … ```blocks first. - Falls back to bracket-matching on
{…}and[…]substrings. - Tries
json.loadsfirst, thenast.literal_evalfor single-quoted Python dicts. - Returns the largest valid candidate, or
Noneif nothing parsed.
name and description attributes
Every agent instance carries a name (used as the router key) and a description (included in the Planner’s system prompt so the LLM knows which agents are available).
Agent Implementations
Planner
File: The tasks are loaded into an internal
src/agent/planner.pyThe Planner is the pipeline’s brain. On its first call it builds a planning prompt that includes the user’s query and the name → description map of all registered agents, then asks the LLM to return an ordered task list:deque. On each subsequent call the Planner pops the next task and routes to the corresponding agent. When the queue is empty it emits {"agent": "TERMINATE", "task": "TERMINATE", "data": [...]}.Quick_searcher
File: Results are appended to the shared
src/agent/quick_searcher.pyProvides fast, low-latency web search by delegating to DuckSearch (DuckDuckGo). For each result it normalises the fields into the shared data schema:data list and the agent routes back to planner for the next task.Search_agent
File:
src/agent/search.pyA more thorough research agent that uses the Crawl (crawl4ai / Playwright) backend for JavaScript-rendered pages. Its internal workflow:_plan(task)— asks the LLM for a tool-call list (url_searchthenpage_contentsteps).url_search— callscrawl.get_url_llm(url, query)to extract relevant links from a search engine results page.page_content— callscrawl.get_summary(urls, query)to fetch each URL and produce a structured{title, summary, brief_summary, keywords, url}object.
self.db and are returned to planner when all steps are complete.Reporter
File:
src/agent/reporter.pyTurns accumulated search data into a polished report. Its workflow:data_handler(data)— assigns each data item a random short ID and extractssummaryfields into a compact reference list._planner(query, db)— asks the LLM to outline report sections, each referencing the IDs of relevant sources._task_handler(tasks)— iterates over sections, fetches the full source records for each, and callsreport_task()to write that section with a focused LLM prompt.- Concatenates all sections and returns
{"agent": "TERMINATE", "data": <final_report>, "task": ""}.
RAG_agent
File:src/agent/retrival.py
The local retrieval agent ingests files from a configured directory and answers queries against them using ChromaDB.
Workflow:
- Walks
self.filelistrecursively; converts each file to Markdown withmarkitdown. - Chunks the Markdown into 1 500-character segments and upserts each chunk into ChromaDB with the file path as metadata.
- Queries the vector store for the top-2 most relevant chunks.
- Passes each chunk through the LLM using
retrieval_promptto generate a structured summary. - Appends results to
dataand routes back toplanner.
The Server and Router
TheServer (src/router/server.py) and Router (src/router/router.py) are the coordination layer that connects agents without coupling them directly.
Serverholds adictof namedRouterobjects. It tracks the current active router, the accumulateddatalist, and drives the mainwhile Trueloop inserver.start().Routerwraps a singleAgent. Itsrecv_response(message, data)method callsagent.run(message, data)and returns the result back to the Server.
Server.start():
query_handler extracts the next router from response["agent"], the new task string from response["task"], and the updated data list from response["data"]. The task string — not the full response dict — becomes the query passed to the next agent’s run().
Data Flow Format
Every agent must return a dict with exactly three keys:| Key | Type | Purpose |
|---|---|---|
agent | str | Name of the next router to call, or "TERMINATE" |
task | str | The task string forwarded to the next agent’s run() |
data | list | Accumulated result objects passed through the full pipeline |
Factory Class
src/factory/factory.py provides two static factory methods that decouple agent and model construction from the rest of the codebase:
get_agent maps names to their concrete classes:
| Name | Class |
|---|---|
"planner" | Planner |
"reporter" | Reporter |
"searcher" | Quick_searcher |
"local-retrieval" | RAG_agent |
get_model maps provider strings to the corresponding Model subclass:
| Provider string(s) | Class |
|---|---|
"deepseek" | Deepseek |
"google" or "gemini" | Gemini |
"ollama" | Ollama |
"xAI" or "gork" | Gork |
"openai" or "gpt" | OpenAI |
Constructing a Pipeline
The helper functiongenerate_report() in src/generate_report.py shows the canonical pattern for assembling a pipeline:
generate_report creates one Router per agent (including the planner), registers them all on a shared Server, points the Server at the Planner as the initial router, and calls server.start(query). The function returns the "data" field of the final TERMINATE response — the finished report string.