Simple Internet Search CLI with Agent‑Reach (Python)

A beginner‑friendly clone that queries the web from the command line using free open‑source tools

Welcome to another official UVF IT build guide! In this tutorial we’ll create a minimal command‑line search tool inspired by Agent‑Reach, letting your Python script fetch web results without paying for proprietary APIs.

You’ll spin up a tiny FastAPI server that proxies requests to the free DuckDuckGo Instant Answer API, then call it from a local CLI – all in under 45 minutes.

Before you start…

Architecture Overview
LOCAL ENVIRONMENT HTTP GET /search?q=… GET https://api.duckduckgo.com/?q=…&format=json Search CLI FastAPI Proxy DuckDuckGo JSON API
1

Phase 1: Scaffold the project

Create a fresh directory for the clone, set up an isolated Python environment, and pull in the web framework we’ll need. This gives us a clean workspace and guarantees the right package versions.

We’ll also install Uvicorn, the ASGI server that will run our FastAPI app locally. All of this is done with a few shell commands, no extra tooling required.

mkdir agent_reach_clone && cd agent_reach_clone
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install fastapi uvicorn
2

Phase 2: Core feature – FastAPI proxy that calls DuckDuckGo

We expose a single /search endpoint that forwards the query to DuckDuckGo’s public JSON API and returns the top abstract. The endpoint accepts a q query‑parameter, contacts DuckDuckGo, extracts the first result, and sends it back as plain JSON.

FastAPI makes routing trivial and lets us keep the code readable for beginners. The server will run on http://127.0.0.1:8000 and be ready for the CLI to call.

app.py

from fastapi import FastAPI, HTTPException
import httpx

app = FastAPI()

DDG_ENDPOINT = "https://api.duckduckgo.com/"

@app.get("/search")
async def search(q: str):
    params = {"q": q, "format": "json", "no_redirect": 1, "no_html": 1}
    async with httpx.AsyncClient() as client:
        resp = await client.get(DDG_ENDPOINT, params=params, timeout=10)
    if resp.status_code != 200:
        raise HTTPException(status_code=502, detail="DuckDuckGo service unavailable")
    data = resp.json()
    # Return the first abstract if available, else a friendly message
    abstract = data.get("AbstractText") or "No concise answer found."
    return {"query": q, "answer": abstract}
3

Phase 3: CLI client to call the local API

In this step we create a tiny Python script that reads a search term from the command line and asks our local FastAPI server for results. The script uses httpx – a lightweight async‑friendly HTTP client – to keep the code simple and fast. When the server replies, we extract the human‑readable result field and print it so the user sees the answer directly in the terminal.

search_cli.py

import sys
import httpx

def main():
    if len(sys.argv) < 2:
        print('Usage: python search_cli.py <query>')
        sys.exit(1)
    query = ' '.join(sys.argv[1:])
    try:
        resp = httpx.get('http://127.0.0.1:8000/search', params={'q': query})
        resp.raise_for_status()
        data = resp.json()
        print(data.get('result', 'No result returned'))
    except Exception as e:
        print(f'Error contacting API: {e}')

if __name__ == '__main__':
    main()
4

Phase 4: Run locally and test

Now we start the FastAPI server in the background so the CLI can talk to it, then we invoke the new script with a sample query. Running the server with uvicorn in detached mode lets the terminal stay free for the next command. Finally we call the CLI and watch the printed answer – if you see a sentence about your topic, the clone is working!

# Start the FastAPI server in the background (assumes app is defined in main.py)
nohup uvicorn main:app --host 127.0.0.1 --port 8000 > server.log 2>&1 &
# Give the server a moment to start
sleep 2
# Test the CLI with a sample search term
python search_cli.py "open source tools"
5

Phase 5: Verify and extend

After confirming the plain text output, you can make the CLI richer by adding a --json flag that prints the full JSON payload. Using Python's built‑in argparse keeps the interface familiar and lets you easily add more options later, such as limiting results or switching endpoints. The extended version replaces the previous script but remains backward compatible – it still works without any flags.

The extra flag is useful for developers who want to see the raw data, integrate it into other tools, or debug the API response. Feel free to experiment with additional arguments as your project grows.

search_cli.py

import sys
import argparse
import httpx
import json

def query_api(q: str) -> dict:
    resp = httpx.get('http://127.0.0.1:8000/search', params={'q': q})
    resp.raise_for_status()
    return resp.json()

def main():
    parser = argparse.ArgumentParser(description='Simple internet search via local FastAPI')
    parser.add_argument('query', nargs='+', help='Search terms')
    parser.add_argument('--json', action='store_true', help='Print raw JSON response')
    args = parser.parse_args()
    q = ' '.join(args.query)
    try:
        data = query_api(q)
        if args.json:
            print(json.dumps(data, indent=2))
        else:
            print(data.get('result', 'No result returned'))
    except Exception as e:
        print(f'Error contacting API: {e}', file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()

Verifying the Stack

CheckCommand / Why it matters
Virtual environment activated and dependencies installedpip list | grep -E 'fastapi|uvicorn|httpx'
FastAPI server responds with JSONcurl -s 'http://127.0.0.1:8000/search?q=python' | jq .result
CLI prints a readable resultpython search_cli.py 'open source'

What’s next?

You now have a functional internet‑search CLI built on free services. Experiment by swapping DuckDuckGo for another public API, adding caching with SQLite, or packaging the CLI with pyinstaller for distribution.