diff --git a/.gitignore b/.gitignore index 27e0605..98b0d18 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ venv/ .idea/ .vscode/ .DS_Store + +# Brand asset working files (downloaded by scripts/render_screenshot.py at runtime) +docs/assets/FTSystemMono-*.ttf +docs/assets/parallel-symbol-*.png +docs/assets/parallel-logo-*.png diff --git a/README.md b/README.md index 2e529ea..e1f1dff 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ +

+ Parallel +

+ # parallel-google-adk +[![PyPI](https://img.shields.io/pypi/v/parallel-google-adk.svg)](https://pypi.org/project/parallel-google-adk/) +[![Python](https://img.shields.io/pypi/pyversions/parallel-google-adk.svg)](https://pypi.org/project/parallel-google-adk/) +[![License](https://img.shields.io/pypi/l/parallel-google-adk.svg)](LICENSE) + Google [Agent Development Kit](https://adk.dev) (ADK) tools and plugin for [Parallel](https://parallel.ai) — grounded web search, clean extraction, and cited deep research with structured output. ## Install ```bash -pip install parallel-google-adk -export PARALLEL_API_KEY=your-key-here # get one at https://platform.parallel.ai +uv add parallel-google-adk # or: pip install parallel-google-adk +export PARALLEL_API_KEY=your-key-here # get one at https://platform.parallel.ai ``` ## Quickstart @@ -72,7 +80,9 @@ This package is the typed-FunctionTool path for users who want fine-grained sche ## Examples -See [`examples/research_agent.py`](examples/research_agent.py) for a runnable demo. +See [`examples/research_agent.py`](examples/research_agent.py) for a runnable demo. A run looks like this: + +![Research agent run](docs/assets/research_agent.png) ## Development diff --git a/docs/assets/parallel.png b/docs/assets/parallel.png new file mode 100644 index 0000000..4bb12af Binary files /dev/null and b/docs/assets/parallel.png differ diff --git a/docs/assets/research_agent.png b/docs/assets/research_agent.png new file mode 100644 index 0000000..4ad40f1 Binary files /dev/null and b/docs/assets/research_agent.png differ diff --git a/docs/screenshot.txt b/docs/screenshot.txt index 96b6f5e..840da23 100644 --- a/docs/screenshot.txt +++ b/docs/screenshot.txt @@ -2,15 +2,7 @@ $ python research_agent.py >>> What does Parallel's Search API do? Cite parallel.ai in one sentence. -Parallel’s Search API is a tool designed specifically for AI agents and LLMs to perform natural language web searches and retrieve structured, token-efficient data. Unlike traditional search engines that rely on keywords, it processes a "natural language objective" to return ranked URLs with extended excerpts optimized for model consumption, reducing hallucinations and improving accuracy in complex research tasks ([parallel.ai](https://docs.parallel.ai/search/search-quickstart)). - -According to the developer documentation: "The Search API takes a natural language objective and returns relevant excerpts optimized for LLMs, replacing multiple keyword searches with a single call for broad or complex queries" ([parallel.ai](https://docs.parallel.ai/search/search-quickstart)). - -### Key Features -* **Search Modes:** Offers specialized modes including **one-shot** (comprehensive), **agentic** (concise for multi-step reasoning), and **fast** (latency-sensitive) ([parallel.ai](https://docs.parallel.ai/search/modes)). -* **LLM Optimization:** Excerpts are curated to fit efficiently into a model's context window, lowering token costs while maintaining high relevance ([parallel.ai](https://parallel.ai/)). -* **Accuracy Benchmarks:** Parallel claims its enterprise deep research API achieves up to 48% accuracy on complex benchmarks, significantly outperforming the native browsing capabilities of standard LLMs ([parallel.ai](https://parallel.ai/)). -* **Evidence-Based:** Every output provides verifiability and provenance, ensuring AI agents can cite their sources accurately ([parallel.ai](https://parallel.ai/)). +Parallel's Search API is a web search engine designed specifically for AI agents that streamlines the search, scrape, and extraction pipeline into a single API call using declarative semantic search to provide token-efficient results ([parallel.ai](https://parallel.ai/products/search)). --- Parallel call trace (1 calls) --- - {"tool": "_web_search", "latency_s": 1.4036233751103282, "citation_count": 5} + {"tool": "web_search", "latency_s": 2.6487630419433117, "citation_count": 5} diff --git a/scripts/render_screenshot.py b/scripts/render_screenshot.py new file mode 100644 index 0000000..71528d1 --- /dev/null +++ b/scripts/render_screenshot.py @@ -0,0 +1,173 @@ +"""Render docs/screenshot.txt to a brand-styled PNG for the adk-docs PR. + +Reads docs/screenshot.txt, lays it out in FT System Mono on Off-white using +Parallel's brand palette (per assets.parallel.ai/llms.txt style guide), and +writes docs/assets/research_agent.png. + +Fonts and intermediate brand assets are downloaded on demand from +assets.parallel.ai (gitignored). The committed outputs are docs/assets/parallel.png +(the symbol logo) and docs/assets/research_agent.png (the rendered screenshot). + +Run: + .venv/bin/python scripts/render_screenshot.py +""" + +from __future__ import annotations + +import sys +import urllib.request +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "docs" / "screenshot.txt" +DST = ROOT / "docs" / "assets" / "research_agent.png" + +# Parallel brand palette (assets.parallel.ai/llms.txt). +OFF_WHITE = "#fcfcfa" +INDEX_BLACK = "#1d1b16" +NEURAL = "#d8d0bf" +SIGNAL = "#fb631b" + +FONT_REGULAR = ROOT / "docs" / "assets" / "FTSystemMono-Regular.ttf" +FONT_BOLD = ROOT / "docs" / "assets" / "FTSystemMono-Bold.ttf" + +FONT_URLS = { + FONT_REGULAR: "https://assets.parallel.ai/FTSystemMono-Regular.ttf", + FONT_BOLD: "https://assets.parallel.ai/FTSystemMono-Bold.ttf", +} + + +def ensure_fonts() -> None: + """Download the brand fonts if they aren't already cached locally. + + assets.parallel.ai's CDN rejects the default urllib User-Agent (403); + pass a browser-style UA so the request goes through. + """ + for path, url in FONT_URLS.items(): + if path.exists(): + continue + path.parent.mkdir(parents=True, exist_ok=True) + print(f"fetching {url}", file=sys.stderr) + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req) as resp, path.open("wb") as fp: + fp.write(resp.read()) + +# Layout. +WIDTH = 1400 +PADDING_X = 70 +PADDING_Y = 60 +FONT_SIZE = 22 +LINE_HEIGHT = int(FONT_SIZE * 1.55) +WRAP_WIDTH = 78 # chars + +# Window-chrome dots (subtle, no labels). +DOT_RADIUS = 7 +DOT_GAP = 22 + + +def wrap(text: str, width: int) -> list[str]: + """Word-wrap a single line of text to a max char width, preserving runs.""" + if len(text) <= width: + return [text] + out: list[str] = [] + line = "" + for word in text.split(" "): + if not line: + line = word + elif len(line) + 1 + len(word) <= width: + line = f"{line} {word}" + else: + out.append(line) + line = word + if line: + out.append(line) + return out + + +def color_for_line(line: str) -> str: + stripped = line.strip() + if stripped.startswith("$ "): + return SIGNAL + if stripped.startswith(">>>"): + return SIGNAL + if stripped.startswith("---"): + return NEURAL + if stripped.startswith("{"): + return NEURAL + return INDEX_BLACK + + +def font_for_line(line: str, regular: ImageFont.FreeTypeFont, bold: ImageFont.FreeTypeFont) -> ImageFont.FreeTypeFont: + s = line.strip() + if s.startswith("$ ") or s.startswith(">>>"): + return bold + return regular + + +def main() -> int: + if not SRC.exists(): + print(f"missing: {SRC}", file=sys.stderr) + return 1 + ensure_fonts() + + raw = SRC.read_text().splitlines() + + # Wrap long lines once for layout. + lines: list[str] = [] + for line in raw: + if not line: + lines.append("") + else: + lines.extend(wrap(line, WRAP_WIDTH)) + + regular = ImageFont.truetype(str(FONT_REGULAR), FONT_SIZE) + bold = ImageFont.truetype(str(FONT_BOLD), FONT_SIZE) + + # Compute height: window-chrome row + padding + text + padding. + chrome_height = PADDING_Y - 10 + text_height = max(LINE_HEIGHT * len(lines), 100) + height = chrome_height + PADDING_Y + text_height + PADDING_Y + + img = Image.new("RGB", (WIDTH, height), OFF_WHITE) + draw = ImageDraw.Draw(img) + + # Window chrome dots. + cy = chrome_height // 2 + 18 + cx = PADDING_X + DOT_RADIUS + for i in range(3): + x = cx + i * DOT_GAP + draw.ellipse( + [(x - DOT_RADIUS, cy - DOT_RADIUS), (x + DOT_RADIUS, cy + DOT_RADIUS)], + outline=NEURAL, + width=2, + ) + + # Hairline separator between chrome and text. + sep_y = chrome_height + PADDING_Y // 2 + draw.line( + [(PADDING_X, sep_y), (WIDTH - PADDING_X, sep_y)], + fill=NEURAL, + width=1, + ) + + # Text. + y = sep_y + PADDING_Y // 2 + for line in lines: + if line == "": + y += LINE_HEIGHT + continue + font = font_for_line(line, regular, bold) + color = color_for_line(line) + draw.text((PADDING_X, y), line, font=font, fill=color) + y += LINE_HEIGHT + + DST.parent.mkdir(parents=True, exist_ok=True) + img.save(DST, optimize=True) + print(f"wrote {DST} ({img.size[0]}x{img.size[1]})") + return 0 + + +if __name__ == "__main__": + sys.exit(main())