Compare commits

...
13 Commits
Author SHA1 Message Date
MervinPraison 17aa27739e Release v0.2.46 2026-10-31 20:34:10 +00:00
praisonai-triage-agent[bot] fb6fb48816 Merge pull request #118 from MervinPraison/claude/issue-6168-20261031
feat: add You.com tools (migrated from praisonaiagents core)
2026-10-31 20:33:07 +00:00
MervinPraisonandClaude Opus 5.5 0fac8a0aa5 fix: honour explicit YouTools api_key, expose ydc alias, tighten missing-key test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 18:21:52 +00:00
MervinPraisonandClaude Opus 5.5 072585870e feat: add You.com tools migrated from praisonaiagents core (fixes MervinPraison/PraisonAI#6168)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 18:17:30 +00:00
Mervin PraisonandClaude Opus 5.5 436a66e163 ci: bump github-automation-template to v1.1.20 (#117)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-29 02:38:18 +01:00
Mervin PraisonandClaude Opus 5.5 e18de78873 ci: bump github-automation-template to v1.1.19 (#116)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-26 11:42:24 +01:00
Mervin PraisonandClaude Opus 5.5 b8ee7dc5b2 chore(ci): github-automation-template v1.1.18 (#115)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-26 09:46:30 +01:00
Mervin PraisonandClaude Opus 5.5 edb4a66564 fix(ci): github-automation-template v1.1.17 (permission ceiling) (#114)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-17 09:34:43 +01:00
Mervin PraisonandClaude Opus 5.5 d332f6e28d chore(ci): github-automation-template v1.1.16 (#113)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-17 09:27:20 +01:00
d3de3800a2 chore(ci): bump github-automation-template to v1.1.15 (#112)
* chore(ci): bump github-automation-template to v1.1.15

Writers-only auto-Claude, least-privilege tokens for Claude on fork PRs and
in the merge-gate assessor, merge-gate held-out BLOCKs (no repeat verdicts on
an unchanged HEAD), portable prompts. auto-pr-comment sweep moves to hourly so
stale FINAL reviews are re-posted once a push settles.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: sync automation.meta.json hubRef to v1.1.15

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 16:55:55 +01:00
Mervin PraisonandClaude Opus 5.5 7dbfbdf5ae fix(ci): github-automation-template v1.1.14 (fork PR isolation) (#111)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 08:58:50 +01:00
Mervin PraisonandClaude Opus 5.5 47c30e3c1d fix(ci): require write access to invoke @claude (template v1.1.13) (#110)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 08:30:31 +01:00
MervinPraisonandCursor c16bd22c6c chore(ci): bump github-automation-template hub to v1.1.12 (Opus/Sonnet 5.5)
Co-authored-by: Cursor <[email protected]>
2026-10-11 22:33:21 +01:00
11 changed files with 585 additions and 19 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
"stack": "claude",
"profile": "tools",
"mode": "hub",
"hubRef": "v1.1.11",
"hubRef": "v1.1.15",
"templateVersion": "1.1.6",
"installedAt": "2026-09-07T12:54:25Z"
}
+3 -3
View File
@@ -10,7 +10,7 @@ on:
pull_request_target:
types: [opened, synchronize]
schedule:
- cron: '30 3,9,15,21 * * *'
- cron: '30 * * * *'
workflow_dispatch:
env:
@@ -27,7 +27,7 @@ jobs:
permissions:
issues: write
pull-requests: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].11
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
+2 -2
View File
@@ -16,8 +16,8 @@ permissions:
jobs:
recover:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].11
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
pr_number: ${{ github.event.inputs.pr_number || '' }}
+2 -2
View File
@@ -25,8 +25,8 @@ permissions:
jobs:
detect-and-trigger:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].11
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
pr_number: ${{ github.event.inputs.pr_number || '' }}
+5 -4
View File
@@ -43,9 +43,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].11
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
@@ -87,9 +87,10 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
statuses: write # the assess job records security/outsider-review
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].11
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].11
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
+2 -2
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
sync:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].11
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
secrets: inherit
with:
hub_ref: v1.1.11
hub_ref: v1.1.20
+484
View File
@@ -0,0 +1,484 @@
"""You.com search and content tools for PraisonAI Agents.
Migrated from ``praisonaiagents.tools.youdotcom_tools`` (MervinPraison/PraisonAI#6168).
You.com provides AI-powered web search, news search, content extraction, and image search.
This module requires the YDC_API_KEY environment variable to be set.
Usage:
from praisonai_tools.tools.youdotcom_tool import ydc_search, ydc_news, ydc_contents
# Search the web (unified web + news)
results = ydc_search("AI news 2024")
# Get live news
news = ydc_news("technology trends")
# Extract content from URLs
content = ydc_contents(["https://example.com"])
# Or use the class directly
from praisonai_tools.tools.youdotcom_tool import YouTools
you = YouTools()
results = you.search("AI news")
"""
from typing import List, Dict, Any, Optional, Union
import logging
import os
from importlib import util
class YouTools:
"""Comprehensive tools for web search and content using You.com API.
You.com provides AI-powered search capabilities optimized for LLM applications.
Features:
- Unified web and news search with LLM-ready snippets
- Live news search from authoritative sources
- Content extraction from URLs (HTML/Markdown)
- Image search
- Advanced search operators support
- Geographic and language targeting
- Freshness controls
Requires:
- youdotcom package: pip install youdotcom
- YDC_API_KEY environment variable
"""
def __init__(self, api_key: Optional[str] = None):
"""Initialize YouTools.
Args:
api_key: Optional API key. If not provided, uses YDC_API_KEY env var.
"""
self._api_key = api_key
self._client = None
def _get_api_key(self) -> Optional[str]:
"""Get API key from instance or environment."""
return self._api_key or os.environ.get("YDC_API_KEY")
def _get_client(self):
"""Get or create You.com client."""
if self._client is None:
if util.find_spec("youdotcom") is None:
return None, "youdotcom package is not installed. Please install it using: pip install youdotcom"
api_key = self._get_api_key()
if not api_key:
return None, "YDC_API_KEY environment variable is not set. Please set it to use You.com tools."
from youdotcom import You
self._client = You(api_key_auth=api_key)
return self._client, None
def search(
self,
query: str,
count: int = 10,
freshness: Optional[str] = None,
country: Optional[str] = None,
language: Optional[str] = None,
offset: int = 0,
safesearch: str = "moderate",
livecrawl: Optional[str] = None,
livecrawl_formats: Optional[str] = None,
) -> Dict[str, Any]:
"""Search the web using You.com's unified search API.
Returns both web and news results in a single call with LLM-ready snippets.
Args:
query: Search query (supports operators like site:, filetype:, +term, -term)
count: Max results per section (default 10, max 100)
freshness: Filter by recency - "day", "week", "month", "year" or "YYYY-MM-DDtoYYYY-MM-DD"
country: ISO country code (e.g., "US", "GB", "FR")
language: BCP 47 language code (e.g., "EN", "JP", "DE")
offset: Pagination offset (0-9), results = offset * count
safesearch: Content filter - "off", "moderate", "strict"
livecrawl: Fetch full content - "web", "news", or "all"
livecrawl_formats: Content format - "html" or "markdown"
Returns:
Dict containing:
- results: {web: [...], news: [...]}
- metadata: {query, search_uuid, latency}
Search Operators:
- site:domain.com - Search within specific domain
- filetype:pdf - Filter by file type
- +term - Must include term
- -term - Exclude term
- AND, OR, NOT - Boolean operators
"""
client, error = self._get_client()
if error:
logging.error(error)
return {"error": error}
try:
kwargs = {"query": query, "count": count, "offset": offset}
if freshness:
kwargs["freshness"] = freshness
if country:
kwargs["country"] = country
if language:
kwargs["language"] = language
if safesearch:
kwargs["safesearch"] = safesearch
if livecrawl:
kwargs["livecrawl"] = livecrawl
if livecrawl_formats:
kwargs["livecrawl_formats"] = livecrawl_formats
response = client.search.unified(**kwargs)
# Convert response to dict if it's a model object
if hasattr(response, 'model_dump'):
return response.model_dump()
elif hasattr(response, 'to_dict'):
return response.to_dict()
elif hasattr(response, '__dict__'):
return self._convert_response(response)
return response
except Exception as e:
error_msg = f"You.com search error: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def _convert_response(self, response) -> Dict[str, Any]:
"""Convert SDK response object to dictionary."""
result = {}
if hasattr(response, 'results'):
results = response.results
result['results'] = {}
if hasattr(results, 'web') and results.web:
result['results']['web'] = [
{
'url': getattr(r, 'url', ''),
'title': getattr(r, 'title', ''),
'description': getattr(r, 'description', ''),
'snippets': getattr(r, 'snippets', []),
'thumbnail_url': getattr(r, 'thumbnail_url', None),
'page_age': getattr(r, 'page_age', None),
'favicon_url': getattr(r, 'favicon_url', None),
'authors': getattr(r, 'authors', []),
}
for r in results.web
]
if hasattr(results, 'news') and results.news:
result['results']['news'] = [
{
'url': getattr(r, 'url', ''),
'title': getattr(r, 'title', ''),
'description': getattr(r, 'description', ''),
'thumbnail_url': getattr(r, 'thumbnail_url', None),
'page_age': getattr(r, 'page_age', None),
}
for r in results.news
]
if hasattr(response, 'metadata'):
meta = response.metadata
result['metadata'] = {
'query': getattr(meta, 'query', ''),
'search_uuid': getattr(meta, 'search_uuid', ''),
'latency': getattr(meta, 'latency', 0),
}
return result
def get_contents(
self,
urls: Union[str, List[str]],
format: str = "markdown",
) -> Dict[str, Any]:
"""Fetch content from URLs using You.com's content extraction API.
Args:
urls: Single URL or list of URLs to fetch content from
format: Content format - "html" or "markdown" (default: markdown)
Returns:
Dict containing array of results with:
- url: The fetched URL
- title: Page title
- html: HTML content (if format="html")
- markdown: Markdown content (if format="markdown")
- metadata: {site_name, favicon_url}
"""
client, error = self._get_client()
if error:
logging.error(error)
return {"error": error}
try:
if isinstance(urls, str):
urls = [urls]
# Use the contents endpoint
response = client.search.contents(urls=urls, format=format)
# Convert response
if hasattr(response, 'model_dump'):
return {"results": response.model_dump()}
elif isinstance(response, list):
return {"results": [
{
'url': getattr(r, 'url', ''),
'title': getattr(r, 'title', ''),
'html': getattr(r, 'html', None),
'markdown': getattr(r, 'markdown', None),
'metadata': {
'site_name': getattr(getattr(r, 'metadata', None), 'site_name', None) if hasattr(r, 'metadata') else None,
'favicon_url': getattr(getattr(r, 'metadata', None), 'favicon_url', None) if hasattr(r, 'metadata') else None,
}
}
for r in response
]}
return {"results": response}
except Exception as e:
error_msg = f"You.com contents error: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def live_news(
self,
query: str,
count: Optional[int] = None,
) -> Dict[str, Any]:
"""Search for live news using You.com's news API.
Note: This API may require early access. Contact [email protected] for access.
Args:
query: News search query
count: Maximum number of news results to return
Returns:
Dict containing:
- news: {query, results: [...], metadata}
- Each result has: title, description, url, page_age, source_name, thumbnail
"""
client, error = self._get_client()
if error:
logging.error(error)
return {"error": error}
try:
kwargs = {"q": query}
if count:
kwargs["count"] = count
# Try to access live news endpoint
if hasattr(client, 'livenews'):
response = client.livenews.get(**kwargs)
else:
# Fallback: use unified search with news focus
return self.search(query=query, count=count or 10)
if hasattr(response, 'model_dump'):
return response.model_dump()
elif hasattr(response, 'news'):
news = response.news
return {
'news': {
'query': getattr(getattr(news, 'query', None), 'original', query) if hasattr(news, 'query') else query,
'results': [
{
'title': getattr(r, 'title', ''),
'description': getattr(r, 'description', ''),
'url': getattr(r, 'url', ''),
'page_age': getattr(r, 'page_age', None),
'age': getattr(r, 'age', None),
'source_name': getattr(r, 'source_name', None),
'thumbnail': getattr(getattr(r, 'thumbnail', None), 'src', None) if hasattr(r, 'thumbnail') else None,
}
for r in getattr(news, 'results', [])
],
'metadata': {
'request_uuid': getattr(getattr(news, 'metadata', None), 'request_uuid', None) if hasattr(news, 'metadata') else None,
}
}
}
return response
except Exception as e:
error_msg = f"You.com live news error: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def images(
self,
query: str,
) -> Dict[str, Any]:
"""Search for images using You.com's image search API.
Note: This API may require early access. Contact [email protected] for access.
Args:
query: Image search query
Returns:
Dict containing:
- images: {results: [...]}
- metadata: {query, search_uuid}
- Each result has: title, page_url, image_url
"""
client, error = self._get_client()
if error:
logging.error(error)
return {"error": error}
try:
if hasattr(client, 'images'):
response = client.images.get(q=query)
else:
return {"error": "Image search not available in this SDK version"}
if hasattr(response, 'model_dump'):
return response.model_dump()
elif hasattr(response, 'images'):
images = response.images
return {
'images': {
'results': [
{
'title': getattr(r, 'title', ''),
'page_url': getattr(r, 'page_url', ''),
'image_url': getattr(r, 'image_url', ''),
}
for r in getattr(images, 'results', [])
]
},
'metadata': {
'query': getattr(getattr(response, 'metadata', None), 'query', query) if hasattr(response, 'metadata') else query,
'search_uuid': getattr(getattr(response, 'metadata', None), 'search_uuid', None) if hasattr(response, 'metadata') else None,
}
}
return response
except Exception as e:
error_msg = f"You.com images error: {str(e)}"
logging.error(error_msg)
return {"error": error_msg}
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
if self._client is not None:
# Close client if it has a close method
if hasattr(self._client, 'close'):
self._client.close()
elif hasattr(self._client, '__exit__'):
self._client.__exit__(exc_type, exc_val, exc_tb)
return False
# Standalone functions for direct import
def ydc_search(
query: str,
count: int = 10,
freshness: Optional[str] = None,
country: Optional[str] = None,
language: Optional[str] = None,
offset: int = 0,
safesearch: str = "moderate",
livecrawl: Optional[str] = None,
livecrawl_formats: Optional[str] = None,
) -> Dict[str, Any]:
"""Search the web using You.com's unified search API.
This is a convenience function that creates a YouTools instance.
For repeated calls, consider using YouTools class directly.
Args:
query: Search query (supports operators like site:, filetype:, +term, -term)
count: Max results per section (default 10, max 100)
freshness: Filter by recency - "day", "week", "month", "year"
country: ISO country code (e.g., "US", "GB")
language: BCP 47 language code (e.g., "EN", "JP")
offset: Pagination offset (0-9)
safesearch: Content filter - "off", "moderate", "strict"
livecrawl: Fetch full content - "web", "news", or "all"
livecrawl_formats: Content format - "html" or "markdown"
Returns:
Dict containing search results with web and news sections
"""
tools = YouTools()
return tools.search(
query=query,
count=count,
freshness=freshness,
country=country,
language=language,
offset=offset,
safesearch=safesearch,
livecrawl=livecrawl,
livecrawl_formats=livecrawl_formats,
)
def ydc_contents(
urls: Union[str, List[str]],
format: str = "markdown",
) -> Dict[str, Any]:
"""Fetch content from URLs using You.com's content extraction API.
Args:
urls: Single URL or list of URLs to fetch content from
format: Content format - "html" or "markdown"
Returns:
Dict containing extracted content for each URL
"""
tools = YouTools()
return tools.get_contents(urls=urls, format=format)
def ydc_news(
query: str,
count: Optional[int] = None,
) -> Dict[str, Any]:
"""Search for live news using You.com's news API.
Args:
query: News search query
count: Maximum number of results
Returns:
Dict containing news results
"""
tools = YouTools()
return tools.live_news(query=query, count=count)
def ydc_images(
query: str,
) -> Dict[str, Any]:
"""Search for images using You.com's image search API.
Args:
query: Image search query
Returns:
Dict containing image results
"""
tools = YouTools()
return tools.images(query=query)
# Alias for simple usage
ydc = ydc_search
__all__ = ["YouTools", "ydc_search", "ydc_contents", "ydc_news", "ydc_images", "ydc"]
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "praisonai-tools"
version = "0.2.45"
version = "0.2.46"
description = "Extended tools for PraisonAI Agents"
authors = [
{name = "Mervin Praison"}
+81
View File
@@ -0,0 +1,81 @@
"""Unit tests for You.com tools (no network, no API key required)."""
from unittest.mock import MagicMock, patch
from praisonai_tools.tools.youdotcom_tool import (
YouTools,
ydc,
ydc_search,
ydc_contents,
ydc_news,
ydc_images,
)
_FIND_SPEC = "praisonai_tools.tools.youdotcom_tool.util.find_spec"
def test_missing_api_key_returns_error(monkeypatch):
monkeypatch.delenv("YDC_API_KEY", raising=False)
with patch(_FIND_SPEC, return_value=object()):
for fn, arg in (
(ydc_search, "q"),
(ydc_contents, "https://example.com"),
(ydc_news, "q"),
(ydc_images, "q"),
):
result = fn(arg)
assert result == {
"error": "YDC_API_KEY environment variable is not set. Please set it to use You.com tools."
}
def test_explicit_api_key_used_without_env(monkeypatch):
monkeypatch.delenv("YDC_API_KEY", raising=False)
fake_module = MagicMock()
with patch(_FIND_SPEC, return_value=object()), patch.dict(
"sys.modules", {"youdotcom": fake_module}
):
client, error = YouTools(api_key="k")._get_client()
assert error is None
assert client is fake_module.You.return_value
fake_module.You.assert_called_once_with(api_key_auth="k")
def test_ydc_alias():
assert ydc is ydc_search
def test_search_uses_client_and_returns_dict():
response = MagicMock()
response.model_dump.return_value = {"results": {"web": [], "news": []}}
client = MagicMock()
client.search.unified.return_value = response
with patch.object(YouTools, "_get_client", return_value=(client, None)):
result = YouTools().search("ai", count=3, freshness="week")
assert result == {"results": {"web": [], "news": []}}
kwargs = client.search.unified.call_args.kwargs
assert kwargs["query"] == "ai"
assert kwargs["count"] == 3
assert kwargs["freshness"] == "week"
def test_search_exception_is_reported_as_error():
client = MagicMock()
client.search.unified.side_effect = RuntimeError("boom")
with patch.object(YouTools, "_get_client", return_value=(client, None)):
result = YouTools().search("ai")
assert "boom" in result["error"]
def test_discovered_via_package():
import praisonai_tools.tools as tools_pkg
assert tools_pkg.YouTools is YouTools
assert tools_pkg.ydc_search is ydc_search
assert tools_pkg.ydc is ydc_search