Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ jobs:
steps:
- name: Check if this is a version bump commit
id: check
# Read commit message via env var, NOT direct ${{ }} interpolation in
# the run script — interpolating user-controllable text into a shell
# heredoc enables command injection via crafted commit messages.
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
COMMIT_MSG="${{ github.event.head_commit.message }}"
if [[ "$COMMIT_MSG" =~ ^chore:\ bump\ version\ to\ ([0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+)?) ]]; then
VERSION="${BASH_REMATCH[1]}"
echo "should_release=true" >> "$GITHUB_OUTPUT"
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
pip install -e ".[dev]"

- name: Run tests
run: pytest -v --ignore=tests/smoke_live.py
# Live-API integration tests under tests/integration/ are skipped via
# pytest addopts in pyproject.toml.
run: pytest -v

lint:
runs-on: ubuntu-latest
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ runner = Runner(agent=agent, app_name="my-app", plugins=[ParallelTracingPlugin()
If you only want Search and don't want a Python dependency, the [Parallel Search MCP](https://docs.parallel.ai/integrations/mcp/search-mcp) works with ADK's `MCPToolset`:

```python
import os
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPServerParams
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

MCPToolset(
connection_params=StreamableHTTPServerParams(
connection_params=StreamableHTTPConnectionParams(
url="https://search.parallel.ai/mcp",
headers={"Authorization": f"Bearer {os.environ['PARALLEL_API_KEY']}"},
),
Expand All @@ -77,7 +78,8 @@ See [`examples/research_agent.py`](examples/research_agent.py) for a runnable de

```bash
pip install -e ".[dev]"
pytest
pytest # unit tests (no network)
pytest tests/integration/ # live API smoke (needs PARALLEL_API_KEY + GOOGLE_API_KEY)
```

## License
Expand Down
16 changes: 12 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "parallel-google-adk"
version = "0.1.0"
version = "0.0.1"
description = "Google ADK FunctionTools and Plugin for Parallel APIs (search, extract, deep research)."
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -23,8 +23,13 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"google-adk>=1.7",
"parallel-web>=0.3",
# google-adk 1.31.x is the version we test against; older minor versions
# have different plugin/MCPToolset APIs that this package doesn't support.
"google-adk>=1.31",
# parallel-web 0.5.x is required: client.search() expects max_results
# inside advanced_settings, and task_run.execute() / .result() shapes
# are used heavily.
"parallel-web>=0.5.1",
"pydantic>=2.0",
]

Expand All @@ -38,7 +43,7 @@ dev = [

[project.urls]
Homepage = "https://parallel.ai"
Documentation = "https://docs.parallel.ai/integrations/google-adk"
Documentation = "https://github.com/parallel-web/parallel-google-adk#readme"
Repository = "https://github.com/parallel-web/parallel-google-adk"
Issues = "https://github.com/parallel-web/parallel-google-adk/issues"

Expand All @@ -48,3 +53,6 @@ packages = ["src/parallel_google_adk"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
# Skip the live-API integration scripts on default `pytest` runs. Run them
# manually with: pytest tests/integration/ or `python tests/integration/<file>`.
addopts = "--ignore=tests/integration"
4 changes: 1 addition & 3 deletions scripts/capture_screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ async def main() -> int:
plugins=[ParallelTracingPlugin()],
)

prompt = (
"What does Parallel's Search API do? Cite parallel.ai in one sentence."
)
prompt = "What does Parallel's Search API do? Cite parallel.ai in one sentence."

buf.write("$ python research_agent.py\n")
buf.write(f"\n>>> {prompt}\n\n")
Expand Down
28 changes: 27 additions & 1 deletion scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,40 @@ main() {
die "working tree is not clean. Commit or stash changes first."
fi

local current new branch
# Must run from main so the release branch forks from a clean upstream tip.
local current_branch
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current_branch" != "main" ]]; then
die "must be on 'main' (currently on '$current_branch'). Run: git checkout main && git pull"
fi

# Make sure we're up to date with origin/main so the release branch is
# forking from the latest tip — otherwise the version bump can land on a
# stale base.
git fetch origin main --quiet
if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]]; then
die "local main is not in sync with origin/main. Run: git pull --ff-only"
fi

local current new branch tag
current="$(get_current_version)"
new="$(calculate_next_version "$current" "$bump")"
branch="release/$new"
tag="v$new"

# Refuse if the tag already exists locally OR on the remote — auto-release
# would otherwise double-tag and fail mid-flight.
if git rev-parse "$tag" >/dev/null 2>&1; then
die "tag $tag already exists locally"
fi
if git ls-remote --tags origin "$tag" | grep -q "refs/tags/$tag$"; then
die "tag $tag already exists on origin"
fi

echo "current: $current"
echo "new: $new"
echo "branch: $branch"
echo "tag: $tag (will be created by auto-release.yml after merge)"
read -rp "proceed? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || die "aborted"

Expand Down
24 changes: 15 additions & 9 deletions src/parallel_google_adk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,27 @@
model="gemini-flash-latest",
name="research_agent",
tools=[web_search, web_fetch, extract, deep_research, enrich],
plugins=[ParallelTracingPlugin()],
)
runner = Runner(agent=agent, app_name="my-app", plugins=[ParallelTracingPlugin()])

For Search alone, the Search MCP at https://search.parallel.ai/mcp is the simpler
default — see the integration page for the MCPToolset wiring. This package is the
typed-FunctionTool path for users who want fine-grained schemas, hidden polling,
and tracing.
For Search alone, the Search MCP at https://search.parallel.ai/mcp is a simpler
default — see the integration docs for the MCPToolset wiring. This package is
the typed-FunctionTool path for users who want fine-grained schemas, hidden
polling, structured enrichment, and tracing.
"""

from .extract import extract
from .search import web_fetch, web_search
from .task import deep_research, enrich
from importlib.metadata import PackageNotFoundError, version

from ._extract import extract
from ._search import web_fetch, web_search
from ._task import deep_research, enrich
from .tracing import ParallelTracingPlugin

__version__ = "0.1.0"
try:
__version__ = version("parallel-google-adk")
except PackageNotFoundError:
# Editable install / source checkout without metadata.
__version__ = "0.0.0+unknown"

__all__ = [
"ParallelTracingPlugin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,18 @@
from ._client import get_client


def _extract(
def extract(
urls: Annotated[
list[str],
"URLs to extract clean content from. 1–20 per call. Fully-qualified "
"http(s) URLs only.",
"URLs to extract content from. 1–20 per call. Fully-qualified http(s) URLs only.",
],
objective: Annotated[
str | None,
"Optional focus: what to keep relevant in the extraction. Improves "
"signal-to-noise on long pages. Omit for full extraction.",
"Optional focus: what to keep relevant. Improves signal-to-noise on long pages.",
] = None,
full_content: Annotated[
bool,
"If True, returns the full cleaned page content. If False (default), "
"returns LLM-optimized excerpts only.",
"If True, returns full cleaned page content. If False (default), returns excerpts only.",
] = False,
) -> dict[str, Any]:
"""Extract clean content from web pages and PDFs in batch.
Expand All @@ -36,10 +33,12 @@ def _extract(
kwargs: dict[str, Any] = {"urls": list(urls)}
if objective is not None:
kwargs["objective"] = objective
# full_content goes inside advanced_settings, not at the top level —
# the SDK rejects it as an unexpected kwarg otherwise.
if full_content:
kwargs["full_content"] = True
kwargs["advanced_settings"] = {"full_content": True}
response = client.extract(**kwargs)
return response.model_dump()


extract = FunctionTool(_extract)
extract = FunctionTool(extract)
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,29 @@
from ._client import get_client


def _web_search(
def web_search(
objective: Annotated[
str,
"Natural-language description of what you want to find. Be specific — "
"the model uses this to score and select the most relevant excerpts.",
"Natural-language description of what you want to find.",
],
queries: Annotated[
list[str],
"1–5 keyword search queries (3–6 words each). Each query is run "
"independently; results are merged and re-ranked.",
"1–5 keyword search queries (3–6 words each).",
],
max_results: Annotated[
int,
"Maximum number of excerpts to return. 1–10 recommended.",
] = 5,
max_chars_total: Annotated[
int | None,
"Optional ceiling on total characters across all excerpts. Useful for "
"controlling token budget on long-running agent loops.",
"Optional ceiling on total characters across all excerpts.",
] = None,
) -> dict[str, Any]:
"""Search the web for current, citation-aware information.

Returns LLM-optimized excerpts with source URLs. Use this when you need
grounded facts the model wouldn't know, or when you don't already have
URLs to fetch. Prefer web_fetch when you have specific URLs in hand.
grounded facts or don't have specific URLs in hand. Prefer web_fetch when
you already know which URL to read.
"""
client = get_client()
kwargs: dict[str, Any] = {
Expand All @@ -54,12 +51,11 @@ def _web_search(
return response.model_dump()


def _web_fetch(
def web_fetch(
url: Annotated[str, "The URL to fetch. Must be a fully-qualified http(s) URL."],
objective: Annotated[
str | None,
"Optional focus for the extraction — what to keep relevant. If omitted, "
"returns the full cleaned content.",
"Optional focus for the extraction — what to keep relevant. Omit for full extraction.",
] = None,
) -> dict[str, Any]:
"""Fetch a single URL and return clean, LLM-ready content.
Expand All @@ -82,5 +78,5 @@ def _web_fetch(
)


web_search = FunctionTool(_web_search)
web_fetch = FunctionTool(_web_fetch)
web_search = FunctionTool(web_search)
web_fetch = FunctionTool(web_fetch)
Loading
Loading