ai-url-aggregator
Note: This is a small experimental library, provided as-is.
ai-url-aggregator is a Python tool that leverages Perplexity and OpenAI to search the internet for relevant URLs: it expands your query into several searches, runs them, normalizes, deduplicates and checks the results, and then classifies and ranks them with GPT.
Features
- Query Expansion
An OpenAI model turns your query into several search queries, varying the wording, the language and the angle (for example, directories), to find more of the websites you’re after. - Web Search
Perplexity’s Search API runs those queries (up to 5 per request, each request billed once) and returns ranked pages with their title and an extract. Every URL comes from the search index, so none are made up. - Clean & Filter
- Normalizes URLs: adds
https://to bare domains such aswww.example.com, lowercases the host, and drops trailing slashes and fragments. - Removes duplicates, preferring
https://overhttp://and treatingwww.example.comandexample.comas the same site.
- Normalizes URLs: adds
- Online Check
- Verifies each URL’s availability (final status
200or403) concurrently withrequests, following redirects and falling back fromHEADtoGET. - Skips URLs, including redirect targets, that resolve to private, loopback or other non-public addresses. This is a best-effort guard against SSRF: the host is resolved again when connecting, so it doesn't stop DNS rebinding.
- Verifies each URL’s availability (final status
- Relevance Ranking
- Uses an OpenAI model to pick the pages relevant to your query, judging each by its title and extract, classifying it (official site, directory, article or other) and ordering them from most to least relevant. Only URLs from the search results are kept.
Installation
1. Install via PyPI
Requires Python 3.11 or later.
uv add ai-url-aggregator
The package is imported as ai_url_aggregator, with underscores.
2. Set Environment Variables
You must provide your Perplexity and OpenAI API keys:
export PERPLEXITY_API_KEY="PERPLEXITY_API_KEY"
export OPENAI_API_KEY="OPENAI_API_KEY"
Replace "PERPLEXITY_API_KEY" and "OPENAI_API_KEY" with your actual API keys. They are read the first time an API is called, so importing the package works without them. You can also pass your own clients instead (see Configuration).
3. (Optional) Install from Source
- Clone or Download this repository.
- Install Dependencies:
uv syncThis ensures all required libraries (likeopenai,perplexityai,requests, etc.) are installed, along withpytest. - Run the Tests:
uv run pytest
The tests use fake API clients and a local HTTP server, so they need neither API keys nor internet access.
How It Works
-
expand_query(query: str, num_queries: int = 5, ...) -> list[str]- Returns
queryfollowed by up tonum_queries - 1related queries written by an OpenAI model (gpt-6-lunawith low reasoning effort by default). - If the model refuses or its output is cut off, it logs a warning and returns just
query.
- Returns
-
search_web(queries: Sequence[str], ...) -> list[SearchResult]- Runs the queries through Perplexity’s Search API, 5 per request.
- Returns a
SearchResultwith theurl,titleandsnippetof each page found.
-
keep_https(urls: list[str]) -> list[str]- Normalizes the URLs (see
normalize_url) and removes duplicates, preferringhttps://and ignoringwww.. - Drops anything that isn’t an
http(s)URL.
- Normalizes the URLs (see
-
check_urls_online(urls: list[str], ...) -> list[str]- Checks the URLs concurrently and keeps the reachable ones (final status
200or403). - Follows redirects, and tries
GETwhenHEADfails or returns another status, without downloading the body. - Treats URLs that resolve to non-public addresses as offline unless
allow_private_addresses=True.
- Checks the URLs concurrently and keeps the reachable ones (final status
-
search_for_web_urls(query: str, num_queries: int = 5, ...) -> list[SearchResult]- Brings all the above together:
- Expands the query.
- Searches the web.
- Normalizes and deduplicates the results, preferring HTTPS.
- Returns the results whose URLs are online.
- Brings all the above together:
-
get_top_relevant_websites(results: Sequence[SearchResult | str], query: str | None = None, ...) -> list[Website]- Uses an OpenAI model to select the results relevant to
query, judging each by its title and snippet, and returns them asWebsite(name, url, kind), most relevant first.kindis"official_site","directory","article"or"other". - Accepts plain URLs too, although the model then has only the URL to go on.
- Discards any URL the model returns that isn’t in the list.
- Returns an empty list, and logs a warning, if the model refuses or its output is cut off.
- Uses an OpenAI model to select the results relevant to
The OpenAI calls use the Responses API with store=False, so the responses aren’t stored for later retrieval.
Configuration
search_for_web_urls accepts these keyword arguments:
| Argument | Default | Description |
|---|---|---|
num_queries |
5 |
Search queries to run, counting yours; the OpenAI model writes the rest. |
max_results |
20 |
Results per search query, the most Perplexity returns. |
country |
None |
ISO 3166-1 alpha-2 code, such as "UY", to favor results from that country. |
timeout |
5 |
Seconds to wait for each request of the online check. |
max_workers |
10 |
URLs checked online at the same time. |
openai_model |
"gpt-6-luna" |
OpenAI model that writes the queries. It must support Structured Outputs. |
openai_reasoning_effort |
"low" |
Reasoning effort for the OpenAI calls. None leaves it out, for models without reasoning such as gpt-4o. |
openai_temperature |
None |
Sampling temperature for the OpenAI calls. Reasoning models only accept it with openai_reasoning_effort="none". None leaves it out. |
perplexity_client |
None |
A perplexity.Perplexity client. |
openai_client |
None |
An openai.OpenAI client. Its backend must support the Responses API. |
expand_query and get_top_relevant_websites accept the openai_* arguments, search_web accepts max_results, country and perplexity_client, and check_urls_online accepts timeout, max_workers and allow_private_addresses.
For a model without reasoning, such as gpt-4o, leave the reasoning effort out and set a temperature instead:
results = search_for_web_urls(
query="...",
openai_model="gpt-4o",
openai_reasoning_effort=None,
openai_temperature=0,
)
You can pass your own clients; any client you leave out is created from the environment variables:
from openai import OpenAI
from perplexity import Perplexity
from ai_url_aggregator import search_for_web_urls
results = search_for_web_urls(
query="...",
perplexity_client=Perplexity(api_key="..."),
openai_client=OpenAI(api_key="..."),
)
Progress is reported through the standard logging module, under the ai_url_aggregator logger. To see it:
import logging
logging.basicConfig(level=logging.INFO)
Usage Example
Once installed and your environment variables are set, you can do:
from pprint import pprint
from ai_url_aggregator import (
search_for_web_urls,
get_top_relevant_websites
)
# Example query:
query = "Give me a list of all the real estate agencies in Uruguay."
# Step 1: Search the web and keep the unique results that are online
results = search_for_web_urls(query=query, country="UY")
print("--- Online URLs ---")
pprint([result.url for result in results])
# Step 2: Get the websites relevant to the query, classified and ranked
most_important_websites = get_top_relevant_websites(results, query=query)
print("--- Most Important Websites ---")
pprint(most_important_websites)
Example output of step 2 (illustrative; results vary between runs):
[Website(name='InfoCasas', url='https://www.infocasas.com.uy', kind='directory'),
Website(name='CasasWeb', url='https://www.casasweb.com.uy', kind='directory'),
Website(name='Mercado Libre Inmuebles', url='https://www.mercadolibre.com.uy/inmuebles', kind='directory'),
Website(name='Uruguay Inmobiliarias', url='https://www.uruguayinmobiliarias.com', kind='directory')]
License
This project is distributed under the MIT License. See LICENSE for more information.
All suggestions and improvements are welcome!
Metadata
Release files for ai-url-aggregator 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ai_url_aggregator-0.3.0.tar.gz | 19.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ai_url_aggregator-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.1 kB
Release files / ai_url_aggregator-0.3.0.tar.gz
| Download URL | ai_url_aggregator-0.3.0.tar.gz |
|---|---|
| Size | 19.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a78b1315b6ac2ab31a2bc7405d34f74f124af5831b35cdcf8c643658a688ffc3
|
|
BLAKE2b-256 checksum How to use checksums |
ece6c14463870d060631eaf784ab66fed5624e8934225b43e532416b19b3c33c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / ai_url_aggregator-0.3.0-py3-none-any.whl
| Download URL | ai_url_aggregator-0.3.0-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
16bcf23ad69074d73b1062566e99e5b3f2a0082efa9c9de0f0be09f6b04e8e02
|
|
BLAKE2b-256 checksum How to use checksums |
9d668708c35381bcc95a58d8a2ad4a1f1f23874e4374fab9dc50b6ddb1c8590f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for ai-url-aggregator 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ai_url_aggregator-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.