langchain-vera
LangChain integration for Vera, the web data and search API from Waggle: seven tools for agents (read a page, search the web, read a full results page on five engines, keyword ideas, AI answers, a GEO query radar and a company footprint), a document loader for RAG, and a small typed client.
New to Vera? Sign up for free credits. No card needed.
| Class | What it does | Credits |
|---|---|---|
VeraScrapeTool |
Reads a public page and returns its main content as Markdown | 1 (5 with render=True) |
VeraSearchTool |
Web, news or video search | 3 per search, whatever max_results is (1 to 100) |
VeraSerpTool |
A full results page on Google, Bing, DuckDuckGo, Brave or Yahoo: organic results, AI Overview, People Also Ask, optional map listings and shopping | by result |
VeraKeywordIdeasTool |
Keyword ideas for a seed, grouped by intent | by result |
VeraAiAnswersTool |
Asks ChatGPT, Claude or Gemini a question and reports whether a brand is named: mentions, citations, share of voice | by result, the most expensive |
VeraGeoRadarTool |
How likely AI answers are to cite a brand for one query, with forum threads, list articles and question gaps | by result |
VeraCompanyFootprintTool |
How a company shows up in search: own properties, profiles, map listings, third-party mentions | by result |
VeraLoader |
Loads pages as Documents with source metadata |
1 per page (5 with render=True) |
VeraClient |
Typed sync and async client for every route above, plus contacts (1) and products (1) | per call, as above |
A request Vera refuses or cannot complete is not charged.
"By result" means priced by what comes back. Vera works out the most a call can cost and holds that
amount first, you are charged for what was actually served, and the rest is given back. credits_used
(in the tool artifact and on every VeraResponse) is the charge. A results page of up to ten results is
typically 3 credits live and 1 when served from Vera's cache; every further ten results is another page,
and the map listings and shopping add-ons are priced on their own. AI answers are priced per call to each
assistant and cost far more than a search, Claude the most, so the AI answers tool asks one assistant
once unless you say otherwise. On free credits a daily pace also applies to the amount held, so one
expensive call can use up a whole day: a second one the same day raises VeraRateLimitError.
Install
pip install -U langchain-vera
# or
uv add langchain-vera
Python 3.10 or newer, langchain-core 1.x.
Authentication
-
Create a Vera account (free credits on sign-up) and confirm your email.
-
In your Vera workspace open API keys and create a key (
vera_live_...). Copy it when it appears; Vera keeps only a hash. -
Export it:
export VERA_API_KEY="vera_live_..."
Every class reads VERA_API_KEY by default. Pass api_key="..." to override it. Requests go to the
direct Vera API at https://vera.waggle.ae/v1 with Authorization: Bearer <key>; pass base_url= to
point a client, tool or loader somewhere else (for example a staging Vera). The website's /api/vera
proxy is not used: it does not carry the newer routes and gives up on a long request after 40 seconds.
The key is a SecretStr and never appears in a repr.
Tools
from langchain_vera import VeraScrapeTool, VeraSearchTool
search = VeraSearchTool(max_results=5)
scrape = VeraScrapeTool(max_characters=20_000)
print(search.invoke({"query": "open source web scrapers"}))
print(scrape.invoke({"url": "https://example.com"}))
VeraScrapeTool returns the page as Markdown. VeraSearchTool returns a numbered list of titles,
URLs and snippets. All the tools are plain BaseTools with an args_schema, so any tool-calling model
can use them. The model chooses what it asks about (a url, a query, a seed...); everything that
costs credits or context is a setting you control:
| Tool | Settings |
|---|---|
VeraScrapeTool |
render (5 credits instead of 1, for JavaScript-built pages), max_characters (cut long pages to protect the context window) |
VeraSearchTool |
search_type ("web", "news", "videos"), max_results, country ("ae"), language ("ar") |
VeraSerpTool |
engine ("google", "bing", "duckduckgo", "brave", "yahoo"), depth (1 to 100, default 10), gl, hl, location, local_pack, shopping (Google add-ons), fresh, max_characters |
VeraKeywordIdeasTool |
limit (1 to 200, default 50), expand ("none", "questions", "alphabet", "all"), gl, hl, location, fresh, max_characters |
VeraAiAnswersTool |
engines (default ["chatgpt"]), samples (1 to 5, default 1), brand, brand_domain, competitors, country, language, city |
VeraGeoRadarTool |
brand, domain, depth (1 to 100, default 30), location, gl, hl, fresh, max_characters |
VeraCompanyFootprintTool |
depth (1 to 100, default 20), location, gl, hl, fresh, max_characters |
| all | api_key, base_url, timeout (seconds), client (your own VeraClient) |
Settings named gl and hl are Google's country and language codes, as the API names them, for example
gl="ae", hl="ar". A tool's timeout already allows for how long it can take: 60 seconds for the page
and search tools, 50 for a results page, 80 for keyword ideas and the radar, 65 for a company footprint
and 175 for AI answers (the API takes up to about 150).
Results pages
from langchain_vera import VeraSerpTool
tool = VeraSerpTool(gl="ae", hl="en", depth=10)
print(tool.invoke({"query": "best coffee in dubai"})) # a Markdown brief for the model
local = VeraSerpTool(location="Dubai", gl="ae", local_pack=True) # adds the map listings
shops = VeraSerpTool(shopping=True) # adds Google Shopping
The brief covers the organic results (rank, title, URL, snippet, tagged forum, video or shop) and, on
Google, the AI Overview with its citations and People Also Ask. With a tool call the artifact is the
whole results page (organic, serp_features, provenance...).
Keyword ideas
from langchain_vera import VeraKeywordIdeasTool
tool = VeraKeywordIdeasTool(gl="ae", hl="en", expand="questions")
print(tool.invoke({"seed": "specialty coffee"}))
# Keyword ideas for 'specialty coffee' (42 ideas)
#
# question:
# - how to brew specialty coffee
# ...
Ideas come from Google autocomplete, People Also Ask and related searches, each labelled question,
comparison, commercial, local or informational. Ideas only: no search volumes. expand sets how
many autocomplete calls are made: "none" is the cheapest, "all" the most.
AI answers
from langchain_vera import VeraAiAnswersTool
tool = VeraAiAnswersTool(
brand="Roasters",
brand_domain="roasters.ae",
competitors=["Other Roasters"],
engines=["chatgpt"], # "claude" and "gemini" are separate paid calls
samples=1,
country="ae",
)
print(tool.invoke({"prompt": "What are the best specialty coffee roasters in Dubai?"}))
# AI answers for 'What are the best ...?': is Roasters (roasters.ae) named?
# - ChatGPT: Roasters named in 1 of 1 answer (share of voice 40%, average first rank 2, ...)
# Competitors: Other Roasters: named in 1 of 1, average rank 1
# Most cited domains: example.com 50%, roasters.ae 25%
The model can also pass brand, brand_domain and competitors, which override the tool's. Use
samples above 1 for a mention rate. engines is never empty: the API asks every assistant when the
field is left out, which is the most expensive way to run this, so the tool always names them. An
assistant that is not set up answers not_configured and costs nothing. "perplexity" is also accepted
when Vera has it set up. The artifact has the full metrics per assistant (engines[...]["aggregate"]).
GEO radar and company footprint
from langchain_vera import VeraCompanyFootprintTool, VeraGeoRadarTool
radar = VeraGeoRadarTool(brand="Roasters", domain="roasters.ae", location="Dubai", gl="ae")
print(radar.invoke({"query": "best specialty coffee roasters in dubai"}))
footprint = VeraCompanyFootprintTool(location="Dubai", gl="ae")
print(footprint.invoke({"company": "Roasters", "domain": "roasters.ae"}))
The radar returns a citation probability score (0 to 100), the forum threads worth joining, the list articles that name your competitors, the questions your site does not answer yet and a brief for AI writers. The footprint separates the company's own web properties, its social and reference profiles (Wikipedia, LinkedIn, Instagram, Crunchbase), its map listings, what third parties say and the questions people ask about the brand.
Defaults. Leave
locationandglout of the radar or the footprint and the API currently uses Dubai andae. Set them for any other market. AI answers currently useaeandenforcountryandlanguagewhen left out.
With an agent
from langchain.agents import create_agent # pip install langchain
agent = create_agent(
llm,
tools=[
VeraSerpTool(depth=10, max_characters=20_000),
VeraScrapeTool(max_characters=20_000),
VeraKeywordIdeasTool(expand="none"),
],
)
agent.invoke(
{"messages": [("user", "Who ranks for 'specialty coffee dubai' and what do they say?")]}
)
llm is any tool-calling chat model. Add VeraAiAnswersTool only when you want the agent to be able to
spend credits on assistant answers; its description says it is expensive.
Credits and metadata as an artifact
Every tool uses response_format="content_and_artifact". Invoked with a tool call (which is what an
agent does), the ToolMessage carries the credits used and more in its artifact:
message = scrape.invoke(
{"type": "tool_call", "name": "vera_scrape", "id": "1", "args": {"url": "https://example.com"}}
)
message.content # the Markdown
message.artifact # {"url": ..., "title": ..., "status_code": 200, "credits_used": 1, "request_id": ..., "elapsed_ms": ...}
For search, results page, keyword ideas, AI answers, radar and footprint, the artifact is the full
response data (for example results, organic, ideas, engines...) plus credits_used and
request_id.
Errors inside an agent
A request Vera refuses (for example robots.txt disallows a page, the URL is not public, or a field the
API rejects) comes back to the model as an error message, so it can try something else: the tools raise
a ToolException with handle_tool_error=True. Set handle_tool_error=False to raise instead. Errors
the model cannot fix (bad key, no credits, rate limit, server trouble or a timeout) always raise a typed
error, see Errors. An invalid argument the model chose raises ValueError before anything
is sent.
Document loader
from langchain_vera import VeraLoader
loader = VeraLoader(
["https://example.com", "https://example.org"],
continue_on_failure=True, # skip a page Vera cannot read, with a logged warning
)
docs = loader.load() # also: loader.lazy_load(), await loader.aload()
docs[0].page_content # Markdown
docs[0].metadata
# {"source": "https://example.com", "title": "Example Domain", "description": ..., "language": "en",
# "canonical_url": ..., "status_code": 200, "request_id": ..., "credits_used": 1}
metadata["source"] is the URL you asked for. Other keys appear only when Vera returned them (and
final_url when the page redirected). continue_on_failure skips pages that are refused, unreachable
or hit a server error; a bad key, no credits or a rate limit always raise, since every page would fail
the same way. Pass render=True for JavaScript-built pages (5 credits each).
For RAG, split with langchain-text-splitters:
from langchain_text_splitters import MarkdownTextSplitter
chunks = MarkdownTextSplitter(chunk_size=1000).split_documents(docs)
Client
from langchain_vera import VeraClient
client = VeraClient() # reads VERA_API_KEY
page = client.scrape("https://example.com") # 1 credit
page.data["markdown"], page.credits_used, page.request_id
html = client.scrape("https://example.com", format="html") # data["html"]
spa = client.scrape("https://spa.example.com", render=True, wait_for=".results") # 5 credits
prices = client.scrape(
"https://shop.example.com/item",
fields={
"title": "h1",
"price": ".price",
"image": {"selector": "img.hero", "attribute": "src"},
},
) # prices.data["extracted"]
client.search("vera scraping", kind="news", limit=20, country="ae", language="en") # 3 credits
client.contacts("https://example.com/contact") # 1 credit
client.products("https://shop.example.com/item") # 1 credit
# Search tools: priced by what comes back
serp = client.serp("best coffee in dubai", engine="google", gl="ae", hl="en", depth=10)
serp.data["organic"], serp.data["serp_features"], serp.credits_used
client.serp(
"coffee shop",
location="Dubai",
gl="ae",
features=["organic", "paa", "ai_overview", "local_pack"],
) # map listings
ideas = client.keyword_ideas("specialty coffee", gl="ae", expand="questions", limit=50)
ideas.data["ideas"] # [{"keyword": ..., "sources": [...], "intent": ...}]
answers = client.ai_answers(
"What are the best specialty coffee roasters in Dubai?",
"Roasters",
brand_domain="roasters.ae",
competitors=["Other Roasters"],
engines=["chatgpt"],
samples=1,
) # answers.data["engines"][0]["aggregate"]
client.geo_radar_query(
"best specialty coffee roasters in dubai",
brand="Roasters",
domain="roasters.ae",
location="Dubai",
gl="ae",
)
client.company_footprint("Roasters", domain="roasters.ae", location="Dubai", gl="ae")
Every method has an async twin (ascrape, asearch, acontacts, aproducts, aserp,
akeyword_ideas, aai_answers, ageo_radar_query, acompany_footprint). Pass
idempotency_key="..." (16 to 128 letters, digits, _ . : -) to make a retried call free: the same
key and request within 24 hours replays the first answer, with credits_used == 0 and
idempotency_replayed == True. A failed call is replayed too, so retry a failed call with a new key.
How the arguments map to the API:
| Argument | API field |
|---|---|
format="markdown" |
response_format: "markdown" |
format="html" |
return_html: true |
render=True |
render: true |
fields={name: selector or {selector, attribute, multiple}} |
extract |
min_quality_score, wait_ms, wait_for |
the same names |
serp(q, engine=...) |
POST /serp/{engine} with q, gl, hl, location, depth, features, fresh |
keyword_ideas(seed, ...) |
POST /keywords/ideas with seed, gl, hl, location, limit, expand, fresh |
ai_answers(prompt, brand, ...) |
POST /ai/answers with prompt, brand, brand_domain, competitors, engines, samples, country, language, city |
geo_radar_query(query, ...) |
POST /geo/radar/query with query, brand, domain, location, gl, hl, depth, fresh |
company_footprint(company, ...) |
POST /company/footprint with company, domain, location, gl, hl, depth, fresh |
fields are CSS rules read from the static page: up to 10, one tag, #id, .class or [attribute]
selector each, and not with render=True or format="html". Invalid arguments raise ValueError
before anything is sent. serp features beyond the default results page are "local_pack" (the
Google Maps listings) and "shopping", on Google only; ai_answers never sends an empty engines.
There are no automatic retries: a retried call could be charged twice. Retry yourself, with an
idempotency key. Timeouts are explicit: 60 seconds by default with a 10 second connect timeout, and the
long search tools wait as long as they need (see Tools); pass timeout= (seconds or an
httpx.Timeout) to set one for every call.
Errors
All errors derive from langchain_vera.VeraError.
| Error | When | Notes |
|---|---|---|
VeraConfigurationError |
No API key | Also a ValueError |
VeraAuthenticationError |
401 | Bad, expired or revoked key |
VeraInsufficientCreditsError |
402 | Out of credits: top up at https://vera.waggle.ae/pricing |
VeraPermissionError |
403 | For example, email not confirmed yet |
VeraRequestError |
400, 422 and other 4xx | Invalid input, with the API's message (or a page that cannot be read), .code names failed fetches (robots_disallowed...). Not charged |
VeraConflictError |
409, 410 | Idempotency key reused or still running |
VeraRateLimitError |
429 | .retry_after seconds, also the free-account daily pace |
VeraServerError |
5xx | 502 and 503: temporarily unavailable, not charged, .retry_after says when to retry. 504: Vera timed out waiting for the service behind the tool, not charged: try again, or ask for less (a smaller depth, fewer samples, one assistant) |
VeraTimeoutError, VeraConnectionError |
No answer | VeraTimeoutError is a VeraConnectionError |
VeraResponseError |
2xx with an unexpected body |
VeraAPIError is the parent of the HTTP ones. Each carries .status_code, .code, .request_id,
.credits_used (0 means not charged) and .retry_after.
Development
uv sync
uv run ruff check . && uv run ruff format --check .
uv run mypy langchain_vera tests
uv run pytest # unit tests, HTTP mocked, no network
The unit tests include LangChain's standard ToolsUnitTests for every tool and a contract test that
checks every request body against a snapshot of Vera's published OpenAPI request schemas and example
bodies (tests/fixtures, from https://vera.waggle.ae/v1/openapi.json).
The live tests are skipped by default because they spend credits and need a key:
VERA_API_KEY=vera_live_... uv run pytest -m live
# the AI answers test costs far more than the others and needs a second opt-in:
VERA_API_KEY=vera_live_... VERA_LIVE_EXPENSIVE=1 uv run pytest -m live
Publishing (maintainers)
The repository is https://github.com/Studio-Hyve/langchain-vera; [project.urls] in pyproject.toml
already points there. Create it empty and public in the Studio-Hyve organisation, then push this one:
git remote add origin git@github.com:Studio-Hyve/langchain-vera.git
git push -u origin main
Check the package before releasing:
uv sync
uv run ruff check . && uv run mypy langchain_vera tests && uv run pytest # all green first
rm -rf dist && uv build # dist/langchain_vera-<version>.tar.gz and .whl
uvx twine check dist/* # validates the package metadata and README
Optional dry run on TestPyPI (create a token at https://test.pypi.org/manage/account/token/):
uv publish --publish-url https://test.pypi.org/legacy/ --token "$TEST_PYPI_TOKEN"
Release from GitHub Actions without a token, using PyPI trusted publishing:
- On pypi.org open Your account > Publishing and add a pending publisher (the project does not
exist yet): project name
langchain-vera, ownerStudio-Hyve, repositorylangchain-vera, workflowpublish.yml, environmentpypi. - In the GitHub repository open Settings > Environments and create an environment named
pypi(add required reviewers if you want a human to approve each release). - Publish a GitHub release (tag
v0.1.0):.github/workflows/publish.ymlbuilds, runstwine checkanduv publish --trusted-publishing always.
Or publish once with a token (create an API token at https://pypi.org/manage/account/token/; scope it to the project once it exists):
uv publish --token "$PYPI_API_TOKEN"
# or, with twine:
TWINE_USERNAME=__token__ TWINE_PASSWORD="$PYPI_API_TOKEN" uvx twine upload dist/*
Then tag the release (skip this if you released from a GitHub release, which created the tag):
git tag v0.1.0 && git push origin main --tags
To release a new version, bump version in pyproject.toml, add it to CHANGELOG.md, and repeat.
Licence
Metadata
Release files for langchain-vera 0.1.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 | Actions |
|---|---|---|---|---|
| langchain_vera-0.1.0.tar.gz | 55.3 kB | Without provenance | Details for langchain_vera-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| langchain_vera-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for langchain_vera-0.1.0-py3-none-any.whl |
Total release size: 91.0 kB
Release files / langchain_vera-0.1.0.tar.gz
| Download URL | langchain_vera-0.1.0.tar.gz |
|---|---|
| Size | 55.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
14f4b059d709a0b5de52527dd29f655af2ab92c790dc1446250793e5852bab2f
|
|
BLAKE2b-256 checksum How to use checksums |
679444e69617eb9861ed640614ea79323bff82fdc22b160b51d05f509d6b6dc3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / langchain_vera-0.1.0-py3-none-any.whl
| Download URL | langchain_vera-0.1.0-py3-none-any.whl |
|---|---|
| Size | 35.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
08f80a377611892b5ef523a864bb20fab75bb70ab409a6b34942671c458f5b09
|
|
BLAKE2b-256 checksum How to use checksums |
b895f87eed6e193588d6880f21d08ab12985b3185c7266c28933b9348f920429
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for langchain-vera 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.