Skip to content

Commit 2e5bac2

Browse files
TravisHaaitomek
andauthored
feat(knowledge): Tavily connector + web-research wrapper with caching (#1234)
### Why this matters GAIA agents could only reach the web through keyless DuckDuckGo HTML scraping — no Tavily, no result caching, and no way to track or cap API spend. This PR is the Phase 1 foundation for the Knowledge Agent: a `_TAVILY` connector that exposes Tavily `search`/`extract` as MCP tools to **all** agents from one keyring-stored API key, plus a `gaia.web.tavily` wrapper giving cached, credit-budgeted search/extract/crawl with an automatic DuckDuckGo fallback when Tavily isn't configured. Agents get higher-quality web research without re-paying for repeat queries or silently blowing past a credit budget. ### Test plan - [ ] `python -m pytest tests/unit/test_tavily_wrapper.py` — 16 tests (mocked SDK): cache hit/TTL, credit ledger, budget warn/block, DuckDuckGo fallback - [ ] `python -m pytest tests/unit/connectors/test_catalog_docs_url.py` — the new connector's `docs_url` resolves - [ ] `python util/lint.py --black --isort` — clean - [ ] With a key: `gaia connectors configure mcp-tavily --set TAVILY_API_KEY=tvly-…` then `gaia knowledge search "…"` returns Tavily results; without it, the same command falls back to DuckDuckGo - [ ] `gaia knowledge usage` prints the credit ledger ### Open questions for reviewers - **Catalog scope:** `_TAVILY` has no built-in agent consumer yet (the Knowledge Agent lands in a later phase), so there's no `REQUIRED_CONNECTORS` wiring. Acceptable for a foundation PR, or should the entry wait? (@kovtcharov-amd) - **Async `crawl`:** sync `TavilyClient` has `crawl`; `AsyncTavilyClient` doesn't yet — deferred unless you want parity now. Phase 1 of #1141. --------- Co-authored-by: Tomasz Iniewicz <itomek@users.noreply.github.com>
1 parent 4a024ed commit 2e5bac2

9 files changed

Lines changed: 1267 additions & 9 deletions

File tree

docs/connectors/tavily.mdx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
title: "Tavily"
3+
icon: "globe"
4+
description: "Give GAIA agents web search and content extraction via Tavily."
5+
---
6+
7+
<Info>
8+
**Connector ID:** `mcp-tavily` · **Type:** `mcp_server` · **Catalog entry:** [`src/gaia/connectors/catalog/mcp_servers.py`](https://github.com/amd/gaia/blob/main/src/gaia/connectors/catalog/mcp_servers.py)
9+
</Info>
10+
11+
## What you'll need
12+
13+
[Tavily](https://tavily.com) is a web-search API built for AI agents. The
14+
connector is an **MCP server** — GAIA spawns the
15+
[`tavily-mcp`](https://github.com/tavily-ai/tavily-mcp) process on demand via
16+
`npx` and routes tool calls (`tavily-search`, `tavily-extract`) through it, so
17+
the tools become available to **all** GAIA agents.
18+
19+
It needs a single secret: a **Tavily API key**. You'll create one, paste it
20+
into GAIA once, and you're done. The key lives encrypted in your OS keyring;
21+
the MCP server reads it via a `$keyring` reference at launch.
22+
23+
## Step 1 — Get an API key
24+
25+
1. Sign in at <a href="https://app.tavily.com/" target="_blank">app.tavily.com</a>.
26+
2. Copy your API key from the dashboard. It starts with `tvly-` followed by a
27+
string of characters (e.g. `tvly-AbCd…`). If your key doesn't start with
28+
`tvly-`, you're looking at the wrong value.
29+
30+
The free tier includes a monthly credit allowance; a basic search costs 1
31+
credit and an advanced search costs 2.
32+
33+
## Step 2 — Configure GAIA
34+
35+
**From the CLI:**
36+
37+
```bash
38+
gaia connectors configure mcp-tavily --set TAVILY_API_KEY=tvly-...
39+
```
40+
41+
**From the Agent UI:**
42+
43+
1. Launch the Agent UI: `gaia chat --ui`.
44+
2. **Settings** (gear) → **Connections** → click the **Tavily** tile.
45+
3. Paste the key into the **Tavily API Key** field and click **Save**.
46+
47+
Either path stores the key in your OS keyring (a single slot, distinct from
48+
other connectors) and writes a `$keyring` reference into
49+
`~/.gaia/mcp_servers.json` — the key never lives in plaintext on disk.
50+
51+
## Step 3 — Use it
52+
53+
Once configured, the `tavily-search` / `tavily-extract` MCP tools are available
54+
to any agent you grant them to:
55+
56+
```bash
57+
gaia connectors grants grant mcp-tavily builtin:chat --scopes "*"
58+
```
59+
60+
GAIA also ships a Python wrapper (`gaia.web.tavily`) used by web-research
61+
workflows, with response caching, a credit budget, and a CLI:
62+
63+
```bash
64+
gaia knowledge search "AMD ROCm latest release" --max-results 5
65+
gaia knowledge usage # show credits spent
66+
```
67+
68+
<Note>
69+
If the connector isn't configured, `gaia knowledge search` and the wrapper
70+
fall back to a keyless DuckDuckGo search — so search works out of the box,
71+
and Tavily simply upgrades its quality and adds `extract`/`crawl`.
72+
</Note>
73+
74+
## Common issues
75+
76+
### `Unauthorized` / `401` from the MCP server
77+
78+
The key in your keyring is wrong or revoked. Click **Disconnect** on the tile
79+
(or `gaia connectors disconnect mcp-tavily`) and re-add a fresh key.
80+
81+
### `npx: command not found`
82+
83+
`tavily-mcp` is launched via `npx`. Install Node 18+ and ensure `npx` is on
84+
your `PATH`:
85+
86+
```bash
87+
node --version # must be >= 18
88+
which npx # must resolve to a real path
89+
```
90+
91+
### Budget exceeded
92+
93+
`gaia knowledge` warns (if nearing the budget) then blocks once a session passes its `--budget` credit cap. Raise the cap, or pass `--no-block` to warn and proceed instead of blocking.
94+
95+
## Revoking access
96+
97+
- **From GAIA:** Settings → Connections → Tavily → **Disconnect** (or
98+
`gaia connectors disconnect mcp-tavily`). The key is removed from the keyring
99+
and the entry is dropped from `mcp_servers.json`.
100+
- **From Tavily:** rotate or delete the key in your
101+
[Tavily dashboard](https://app.tavily.com/).
102+
103+
## See also
104+
105+
- [Connectors overview](/connectors)
106+
- [Tavily documentation](https://docs.tavily.com/)
107+
- [Connectors security model](/security/connections)

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"connectors/google",
9393
"connectors/microsoft",
9494
"connectors/github",
95+
"connectors/tavily",
9596
"security/connections",
9697
"security/connectors"
9798
]

docs/reference/cli.mdx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,55 @@ gaia cache clear --all
19971997

19981998
---
19991999

2000+
### Knowledge Command
2001+
2002+
Web research via [Tavily](https://tavily.com), with SQLite result caching, a per-session
2003+
credit budget, and an automatic keyless DuckDuckGo fallback when the `mcp-tavily`
2004+
connector isn't configured. See the [Tavily connector](/connectors/tavily).
2005+
2006+
```bash
2007+
gaia knowledge {search,extract,usage} [OPTIONS]
2008+
```
2009+
2010+
**Actions:**
2011+
2012+
| Action | Description |
2013+
|--------|-------------|
2014+
| `search` | Web search for a query. Falls back to DuckDuckGo when Tavily isn't configured. |
2015+
| `extract` | Extract clean content from one or more URLs. Requires the Tavily connector. |
2016+
| `usage` | Print cached credit-usage totals (per operation + session total). |
2017+
2018+
**Options:**
2019+
2020+
| Flag | Type | Applies to | Description |
2021+
|------|------|------------|-------------|
2022+
| `--max-results` | integer | `search` | Maximum results to return (default: 5) |
2023+
| `--depth` | `basic` \| `advanced` | `search`, `extract` | Depth; `advanced` costs more credits (default: `basic`) |
2024+
| `--budget` | integer | `search`, `extract` | Credit cap for the session; omit for unlimited |
2025+
| `--no-block` | flag | `search`, `extract` | Warn instead of blocking when the budget cap is exceeded |
2026+
2027+
**Examples:**
2028+
2029+
<CodeGroup>
2030+
```bash Search the web
2031+
gaia knowledge search "AMD ROCm latest release" --max-results 5
2032+
```
2033+
2034+
```bash Extract page content
2035+
gaia knowledge extract https://example.com/post
2036+
```
2037+
2038+
```bash Show credit usage
2039+
gaia knowledge usage
2040+
```
2041+
</CodeGroup>
2042+
2043+
`search` automatically degrades to DuckDuckGo when the `mcp-tavily` connector isn't
2044+
configured; `extract` requires Tavily and raises an actionable error otherwise. Configure
2045+
the connector with `gaia connectors configure mcp-tavily --set TAVILY_API_KEY=tvly-...`.
2046+
2047+
---
2048+
20002049
### Kill Command
20012050

20022051
Terminate processes running on specific ports.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"python-multipart>=0.0.9",
114114
# gaia connectors is a base CLI command; keyring is its OS credential store (OAuth tokens #915). #1621
115115
"keyring>=24.0.0,<26.0.0",
116+
"tavily-python>=0.5.0",
116117
],
117118
extras_require={
118119
"image": [

src/gaia/cli.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,60 @@ def build_parser():
19121912

19131913
telegram_parser.set_defaults(action="telegram")
19141914

1915+
# Knowledge command — web research via the Tavily wrapper
1916+
knowledge_parser = subparsers.add_parser(
1917+
"knowledge",
1918+
help="Web research via Tavily (search|extract|usage), with caching and a credit budget",
1919+
)
1920+
knowledge_subparsers = knowledge_parser.add_subparsers(
1921+
dest="knowledge_action", help="knowledge action to perform"
1922+
)
1923+
1924+
k_search = knowledge_subparsers.add_parser("search", help="Run a web search")
1925+
k_search.add_argument("query", help="Search query")
1926+
k_search.add_argument(
1927+
"--max-results", type=int, default=5, help="Max results (default: 5)"
1928+
)
1929+
k_search.add_argument(
1930+
"--depth",
1931+
choices=("basic", "advanced"),
1932+
default="basic",
1933+
help="Search depth — advanced costs more credits (default: basic)",
1934+
)
1935+
k_search.add_argument(
1936+
"--budget",
1937+
type=int,
1938+
default=None,
1939+
help="Credit cap for this session; omit for unlimited",
1940+
)
1941+
k_search.add_argument(
1942+
"--no-block",
1943+
action="store_true",
1944+
help="Warn instead of blocking when the budget cap is exceeded",
1945+
)
1946+
1947+
k_extract = knowledge_subparsers.add_parser(
1948+
"extract", help="Extract clean content from one or more URLs (requires Tavily)"
1949+
)
1950+
k_extract.add_argument("urls", nargs="+", help="One or more URLs to extract")
1951+
k_extract.add_argument(
1952+
"--depth",
1953+
choices=("basic", "advanced"),
1954+
default="basic",
1955+
help="Extract depth (default: basic)",
1956+
)
1957+
k_extract.add_argument(
1958+
"--budget", type=int, default=None, help="Credit cap for this session"
1959+
)
1960+
k_extract.add_argument(
1961+
"--no-block",
1962+
action="store_true",
1963+
help="Warn instead of blocking when the budget cap is exceeded",
1964+
)
1965+
1966+
knowledge_subparsers.add_parser("usage", help="Show cached credit-usage totals")
1967+
knowledge_parser.set_defaults(action="knowledge")
1968+
19151969
# Add model download command
19161970
download_parser = subparsers.add_parser(
19171971
"download",
@@ -4167,6 +4221,11 @@ def main():
41674221
handle_cache_command(args)
41684222
return
41694223

4224+
# Handle Knowledge command (Tavily web research)
4225+
if args.action == "knowledge":
4226+
handle_knowledge_command(args)
4227+
return
4228+
41704229
# Handle Memory command
41714230
if args.action == "memory":
41724231
handle_memory_command(args)
@@ -5094,6 +5153,69 @@ def handle_blender_command(args):
50945153
sys.exit(1)
50955154

50965155

5156+
def _print_knowledge_usage(client):
5157+
"""Print a one-line credit-usage summary for a Tavily client."""
5158+
usage = client.usage()
5159+
cap = usage["cap"]
5160+
cap_str = "unlimited" if cap is None else str(cap)
5161+
print(f"\n💳 Credits used: {usage['total_credits']} (cap: {cap_str})")
5162+
5163+
5164+
def handle_knowledge_command(args):
5165+
"""Handle `gaia knowledge` — Tavily web research with caching + budget.
5166+
5167+
Args:
5168+
args: Parsed command-line arguments
5169+
"""
5170+
action = getattr(args, "knowledge_action", None)
5171+
if action is None:
5172+
print("❌ Error: No knowledge action specified")
5173+
print("Available actions: search, extract, usage")
5174+
print("Run 'gaia knowledge --help' for more information")
5175+
return
5176+
5177+
from gaia.web.tavily import (
5178+
BudgetConfig,
5179+
TavilyBudgetExceeded,
5180+
TavilyClient,
5181+
TavilyConfigError,
5182+
)
5183+
5184+
budget = BudgetConfig(
5185+
cap=getattr(args, "budget", None),
5186+
block=not getattr(args, "no_block", False),
5187+
)
5188+
client = TavilyClient(budget=budget)
5189+
try:
5190+
if action == "search":
5191+
result = client.search(
5192+
args.query, search_depth=args.depth, max_results=args.max_results
5193+
)
5194+
source = result.get("source", "tavily")
5195+
print(f"\n=== Results for {args.query!r} (source: {source}) ===")
5196+
for i, r in enumerate(result.get("results", []), 1):
5197+
print(f"{i}. {r.get('title', '')}")
5198+
print(f" {r.get('url', '')}")
5199+
content = r.get("content") or r.get("snippet") or ""
5200+
if content:
5201+
print(f" {content[:200]}")
5202+
_print_knowledge_usage(client)
5203+
elif action == "extract":
5204+
result = client.extract(args.urls, extract_depth=args.depth)
5205+
print(json.dumps(result, indent=2))
5206+
_print_knowledge_usage(client)
5207+
elif action == "usage":
5208+
_print_knowledge_usage(client)
5209+
except TavilyBudgetExceeded as e:
5210+
print(f"🛑 {e}")
5211+
sys.exit(1)
5212+
except TavilyConfigError as e:
5213+
print(f"❌ {e}")
5214+
sys.exit(1)
5215+
finally:
5216+
client.close()
5217+
5218+
50975219
def handle_cache_command(args):
50985220
"""Handle the cache management command.
50995221

src/gaia/connectors/catalog/mcp_servers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@
4949
),
5050
)
5151

52+
_TAVILY = ConnectorSpec(
53+
id="mcp-tavily",
54+
display_name="Tavily",
55+
icon="🛜",
56+
category="dev-tools",
57+
tier=1,
58+
type="mcp_server",
59+
description="Web search and content extraction for agents through the Tavily API.",
60+
docs_url="https://amd-gaia.ai/docs/connectors/tavily",
61+
mcp_command="npx",
62+
mcp_args=("-y", "tavily-mcp@latest"),
63+
mcp_env_keys=("TAVILY_API_KEY",),
64+
config_schema=(
65+
ConfigField(
66+
key="TAVILY_API_KEY",
67+
label="Tavily API Key",
68+
kind="secret",
69+
placeholder="tvly-…",
70+
help_md="Get an API key from your [Tavily dashboard](https://app.tavily.com/).",
71+
secret=True,
72+
),
73+
),
74+
)
75+
5276
_MEMORY = ConnectorSpec(
5377
id="mcp-memory",
5478
display_name="Memory",
@@ -79,6 +103,7 @@
79103

80104
_ALL_SPECS = (
81105
_GITHUB,
106+
_TAVILY,
82107
_MEMORY,
83108
_GIT,
84109
)

0 commit comments

Comments
 (0)