Skip to content
Open
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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ OPENAI_API_KEY=sk-your-api-key-here
# ZHIPU_API_KEY= # Enable Zhipu GLM GEO detection
# DOUBAO_API_KEY= # Enable Doubao (ByteDance) GEO detection

# === Optional: Web search (custom LLM providers) ===
# === Optional: Agent web search ===
# TAVILY_API_KEY= # Enables Tavily web search when using a custom OPENAI_BASE_URL
# OPENCMO_SEARCH_PROVIDER=auto # Agent web_search: auto (existing routing), parallel, or none
# Set parallel for free, anonymous Parallel Search MCP (no search API key or browser).

# === Optional: SEO enhancement ===
# PAGESPEED_API_KEY= # Google PageSpeed Insights (works without key but rate-limited)
Expand Down
22 changes: 22 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ These settings are not required for the first scan, but they unlock richer monit
| Dashboard access | `OPENCMO_WEB_TOKEN`, `OPENCMO_WEB_HOST` | Add these when exposing the dashboard outside local development. |
| Publishing | `REDDIT_CLIENT_ID`, `REDDIT_CLIENT_SECRET`, `TWITTER_API_KEY`, `OPENCMO_AUTO_PUBLISH` | Add these only when you are ready to enable controlled publishing workflows. |

## Agent web search

To give the CMO and specialist agents browser-free web search, set:

```bash
OPENCMO_SEARCH_PROVIDER=parallel
```

This routes their `web_search` tool through [Parallel Search MCP](https://docs.parallel.ai/integrations/mcp/search-mcp)
at `https://search.parallel.ai/mcp`, using the HTTP MCP transport included with the
OpenAI Agents SDK. Anonymous searches use Fast mode and need no Parallel account
or API key. Free access has rate limits and is intended for exploration and light use.
Queries are sent to Parallel, and source URLs and excerpts are returned to the agent.
Requests have a 30-second overall timeout; failures are returned to the agent without
switching providers.

Leave the setting unset or use `auto` to keep the existing Tavily, OpenAI and Google
scraping fallback chain. Existing Tavily credentials continue to work in `auto` mode.
Use `none` to disable this agent tool. This setting only controls the agents'
`web_search` tool; SERP tracking, community scans and page
crawling retain their own integrations.

## Local development notes

- Keep `.env` out of version control.
Expand Down
52 changes: 52 additions & 0 deletions src/opencmo/tools/parallel_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Anonymous Parallel Search MCP through the Agents SDK's HTTP transport."""

from __future__ import annotations

import asyncio

from agents.mcp import MCPServerStreamableHttp

MCP_URL = "https://search.parallel.ai/mcp"
USER_AGENT = "OpenCMO/0.1.0 (https://github.com/Lling0000/OpenCMO)"
_TIMEOUT_SECONDS = 30


async def _search(query: str) -> str:
async with MCPServerStreamableHttp(
name="Parallel Search",
params={
"url": MCP_URL,
"headers": {"User-Agent": USER_AGENT},
"timeout": _TIMEOUT_SECONDS,
"sse_read_timeout": _TIMEOUT_SECONDS,
},
client_session_timeout_seconds=_TIMEOUT_SECONDS,
) as server:
await server.list_tools()
result = await server.call_tool(
"web_search", {"objective": query, "search_queries": [query]},
)
# MCP SDK v2 uses snake_case; v1 exposes the JSON field name.
if getattr(result, "is_error", False) or getattr(result, "isError", False):
raise RuntimeError("Parallel Search MCP returned an error")
# Preserve source URLs and excerpts in the model-visible tool result.
text = "\n\n".join(item.text for item in result.content if item.type == "text")
return text if text.strip() else "No search results found."


async def parallel_search(query: str) -> str:
"""Return search excerpts, bounding discovery and execution together."""
# Older Agents/MCP transports cancel their owning task on HTTP failures.
# Keep that internal cancellation distinct from a user canceling this tool.
task = asyncio.create_task(_search(query))
try:
done, _ = await asyncio.wait({task}, timeout=_TIMEOUT_SECONDS)
if not done:
raise asyncio.TimeoutError("Parallel search timed out")
if task.cancelled():
raise RuntimeError("Parallel search transport was interrupted")
return task.result()
finally:
if not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
16 changes: 16 additions & 0 deletions src/opencmo/tools/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ async def web_search(query: str) -> str:
Args:
query: The search query string.
"""
from opencmo import llm

provider = (llm.get_key("OPENCMO_SEARCH_PROVIDER", "auto") or "auto").strip().lower()
if provider == "none":
return "Web search is disabled."
if provider == "parallel":
from opencmo.tools.parallel_search import parallel_search

try:
return await parallel_search(query)
except Exception:
logger.warning("Parallel web search failed", exc_info=True)
return "Parallel web search failed. Please try again later."
if provider != "auto":
return "Unknown OPENCMO_SEARCH_PROVIDER. Choose auto, parallel, or none."

try:
from opencmo.tools.tavily_helper import tavily_search

Expand Down
Loading