Spaces:
Running
Running
File size: 10,713 Bytes
588592f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | from __future__ import annotations
import os
from typing import Annotated, Any, Literal
import httpx
import gradio as gr
from app import _log_call_end, _log_call_start, _truncate_for_log
from ._docstrings import autodoc
# ===========================================================================
# Constants
# ===========================================================================
BASE_URL = "https://text.pollinations.ai"
# Model mappings for different depth levels
MODEL_MAPPING = {
"fast": "gemini-search",
"normal": "perplexity-fast",
"deep": "perplexity-reasoning",
}
# System prompts for different detail levels
SYSTEM_PROMPTS = {
True: "Search the web and provide a comprehensive answer with sources. Include relevant details and cite your sources.",
False: "Search the web and provide a concise, accurate answer. Include source URLs.",
}
# Timeout settings (seconds)
REQUEST_TIMEOUT = 30.0
# Single source of truth for the LLM-facing tool description
TOOL_SUMMARY = (
"Search the web using AI-powered search models with source citations. "
"Supports different depth levels: fast (Gemini with Google Search), normal (Perplexity Sonar), "
"and deep (Perplexity Sonar Reasoning). Returns answers with source URLs."
)
# ===========================================================================
# Core Client
# ===========================================================================
class PollinationsClient:
"""
HTTP client for Pollinations AI web search API.
Provides web search functionality with different depth levels and citation support.
"""
def __init__(
self,
base_url: str = BASE_URL,
timeout: float = REQUEST_TIMEOUT,
api_key: str | None = None,
) -> None:
"""
Initialize the Pollinations client.
Args:
base_url: Base URL for the Pollinations API (default: https://text.pollinations.ai)
timeout: Request timeout in seconds (default: 30)
api_key: Optional API key (reads from POLLINATIONS_API_KEY env var if not provided)
"""
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.api_key = api_key or os.getenv("POLLINATIONS_API_KEY")
def _get_headers(self) -> dict[str, str]:
"""Get request headers including API key if available."""
headers = {
"Content-Type": "application/json",
}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
return headers
def _resolve_model(self, depth: str) -> str:
"""
Resolve depth level to actual model name.
Args:
depth: Depth level ('fast', 'normal', or 'deep')
Returns:
The model identifier for the Pollinations API
"""
return MODEL_MAPPING.get(depth, "perplexity-fast")
async def web_search(
self,
query: str,
depth: str = "normal",
detailed: bool = False,
) -> dict[str, Any]:
"""
Perform web search using Pollinations AI.
Args:
query: The search query
depth: Search depth level ('fast', 'normal', or 'deep')
detailed: Whether to request a comprehensive answer
Returns:
Dictionary with keys:
- answer: The generated answer
- sources: List of source URLs (citations)
- model: The model used
- query: The original query
Raises:
httpx.HTTPError: For network/HTTP errors
ValueError: For invalid parameters
"""
if not query or not query.strip():
raise ValueError("Query cannot be empty")
if depth not in MODEL_MAPPING:
raise ValueError(f"Invalid depth: {depth}. Must be one of {list(MODEL_MAPPING.keys())}")
model = self._resolve_model(depth)
system_prompt = SYSTEM_PROMPTS.get(detailed, SYSTEM_PROMPTS[False])
# Prepare OpenAI-compatible request
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": query},
],
}
url = f"{self.base_url}/v1/chat/completions"
async with httpx.AsyncClient(timeout=self.timeout) as client:
try:
response = await client.post(
url,
json=payload,
headers=self._get_headers(),
)
response.raise_for_status()
except httpx.TimeoutException as exc:
raise httpx.HTTPError(f"Request timed out after {self.timeout}s") from exc
except httpx.HTTPStatusError as exc:
# Handle rate limiting specifically
if exc.response.status_code == 429:
raise httpx.HTTPError("Rate limited. Please try again later.") from exc
raise
data = response.json()
# Extract answer and citations from response
answer = ""
sources = []
# OpenAI-compatible response format
if "choices" in data and data["choices"]:
answer = data["choices"][0].get("message", {}).get("content", "")
# Extract citations if present (Pollinations-specific extension)
if "citations" in data:
sources = data["citations"]
# Also check if citations are embedded in the message
if not sources and isinstance(answer, str):
# Try to extract URLs from the answer
import re
url_pattern = r'https?://[^\s<>"\'\)]+'
sources = list(dict.fromkeys(re.findall(url_pattern, answer))) # Unique URLs
return {
"answer": answer,
"sources": sources,
"model": model,
"query": query,
}
def web_search_sync(
self,
query: str,
depth: str = "normal",
detailed: bool = False,
) -> dict[str, Any]:
"""
Synchronous version of web_search.
Args:
query: The search query
depth: Search depth level ('fast', 'normal', or 'deep')
detailed: Whether to request a comprehensive answer
Returns:
Dictionary with answer, sources, model, and query
"""
import asyncio
return asyncio.run(self.web_search(query, depth, detailed))
# ===========================================================================
# Gradio Tool Function
# ===========================================================================
@autodoc(
summary=TOOL_SUMMARY,
)
def Pollinations_Web_Search(
query: Annotated[str, "The search query string"],
depth: Annotated[
Literal["fast", "normal", "deep"],
"Search depth: 'fast' (Gemini with Google Search), 'normal' (Perplexity Sonar), or 'deep' (Perplexity Sonar Reasoning).",
] = "normal",
detailed: Annotated[bool, "Request a comprehensive answer instead of concise summary"] = False,
) -> str:
"""
Search the web using Pollinations AI with source citations.
Uses AI-powered search models that provide direct answers with source citations.
Supports three depth levels for different search capabilities.
"""
_log_call_start("Pollinations_Web_Search", query=query, depth=depth, detailed=detailed)
try:
client = PollinationsClient()
result = client.web_search_sync(query, depth, detailed)
# Format the result for display
lines = [
f"Query: {result['query']}",
f"Model: {result['model']}",
f"Depth: {depth}",
"",
"Answer:",
result["answer"] or "No answer generated.",
]
if result["sources"]:
lines.append("")
lines.append("Sources:")
for i, source in enumerate(result["sources"], 1):
lines.append(f" {i}. {source}")
else:
lines.append("")
lines.append("(No sources provided)")
formatted_result = "\n".join(lines)
_log_call_end("Pollinations_Web_Search", _truncate_for_log(formatted_result))
return formatted_result
except ValueError as exc:
error_msg = f"Invalid input: {exc}"
_log_call_end("Pollinations_Web_Search", error_msg)
return error_msg
except httpx.HTTPError as exc:
error_msg = f"Search failed: {exc}"
_log_call_end("Pollinations_Web_Search", error_msg)
return error_msg
except Exception as exc:
error_msg = f"Unexpected error: {exc}"
_log_call_end("Pollinations_Web_Search", error_msg)
return error_msg
# ===========================================================================
# Gradio Interface
# ===========================================================================
def build_interface() -> gr.Interface:
"""Build the Gradio interface for Pollinations web search."""
return gr.Interface(
fn=Pollinations_Web_Search,
inputs=[
gr.Textbox(
label="Query",
placeholder="Enter your search query here...",
max_lines=2,
info="The search query",
),
gr.Radio(
label="Search Depth",
choices=["fast", "normal", "deep"],
value="normal",
info="Search depth level: fast (Gemini), normal (Perplexity), deep (Reasoning)",
),
gr.Checkbox(
label="Detailed Answer",
value=False,
info="Request a comprehensive answer instead of concise summary",
),
],
outputs=gr.Textbox(
label="Search Results",
interactive=False,
lines=15,
max_lines=20,
),
title="Pollinations Web Search",
description=(
"<div style=\"text-align:center\">AI-powered web search with source citations. "
"Uses Google Search, Perplexity Sonar, and Perplexity Sonar Reasoning models "
"to provide direct answers with reliable source URLs.</div>"
),
api_description=TOOL_SUMMARY,
flagging_mode="never",
submit_btn="Search",
)
# ===========================================================================
# Public API
# ===========================================================================
__all__ = [
"PollinationsClient",
"Pollinations_Web_Search",
"build_interface",
] |