composio-scavio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- composio_scavio-0.1.0/.gitignore +8 -0
- composio_scavio-0.1.0/PKG-INFO +92 -0
- composio_scavio-0.1.0/README.md +74 -0
- composio_scavio-0.1.0/composio_scavio/__init__.py +6 -0
- composio_scavio-0.1.0/composio_scavio/tools.py +511 -0
- composio_scavio-0.1.0/cookbook.py +50 -0
- composio_scavio-0.1.0/pyproject.toml +29 -0
- composio_scavio-0.1.0/test_tools.py +98 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: composio-scavio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scavio real-time search tools for Composio (Google, YouTube, Amazon, Walmart, Reddit, TikTok, Instagram)
|
|
5
|
+
Project-URL: Homepage, https://scavio.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/scavio-ai/composio-scavio
|
|
7
|
+
Project-URL: Documentation, https://scavio.dev/docs
|
|
8
|
+
Author-email: Scavio <scavio.dev@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,composio,google,scavio,search,serp,tools
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: composio>=0.17.0
|
|
13
|
+
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: scavio>=0.2.1
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# composio-scavio
|
|
20
|
+
|
|
21
|
+
[Scavio](https://scavio.dev) real-time search tools for [Composio](https://composio.dev).
|
|
22
|
+
|
|
23
|
+
Scavio is a single Search API over Google, YouTube, Amazon, Walmart, Reddit, TikTok, and Instagram. This package exposes those endpoints as a Composio custom toolkit so your agents can pull structured, up-to-date results across any Composio-supported framework (OpenAI, Anthropic, LangChain, CrewAI, and more).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install composio-scavio
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
Get a Scavio API key from the [Scavio Dashboard](https://dashboard.scavio.dev) (new accounts get 50 free credits to start, no credit card). Set it as an environment variable or pass it directly.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export SCAVIO_API_KEY=sk_...
|
|
37
|
+
export COMPOSIO_API_KEY=... # from https://app.composio.dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from composio import Composio
|
|
44
|
+
from composio_scavio import build_scavio_toolkit
|
|
45
|
+
|
|
46
|
+
composio = Composio()
|
|
47
|
+
|
|
48
|
+
# Build the toolkit; expose only the providers you need.
|
|
49
|
+
scavio = build_scavio_toolkit(
|
|
50
|
+
api_key="sk_...", # or rely on SCAVIO_API_KEY
|
|
51
|
+
enable_google=True,
|
|
52
|
+
enable_amazon=True,
|
|
53
|
+
enable_tiktok=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
session = composio.create(
|
|
57
|
+
user_id="user_1",
|
|
58
|
+
experimental={"custom_toolkits": [scavio]},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
result = session.tools.execute(
|
|
62
|
+
"SCAVIO_GOOGLE_SEARCH",
|
|
63
|
+
arguments={"query": "best search API for AI agents", "light_request": True},
|
|
64
|
+
)
|
|
65
|
+
print(result)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Pass `all=True` to register every tool regardless of the individual flags.
|
|
69
|
+
|
|
70
|
+
## Tools
|
|
71
|
+
|
|
72
|
+
All tools are namespaced under the `SCAVIO` toolkit. Each provider is gated by an `enable_*` flag.
|
|
73
|
+
|
|
74
|
+
| Provider | Tools |
|
|
75
|
+
|----------|-------|
|
|
76
|
+
| Google | `SCAVIO_GOOGLE_SEARCH` |
|
|
77
|
+
| Amazon | `SCAVIO_AMAZON_SEARCH`, `SCAVIO_AMAZON_PRODUCT` |
|
|
78
|
+
| Walmart | `SCAVIO_WALMART_SEARCH`, `SCAVIO_WALMART_PRODUCT` |
|
|
79
|
+
| YouTube | `SCAVIO_YOUTUBE_SEARCH`, `SCAVIO_YOUTUBE_METADATA` |
|
|
80
|
+
| Reddit | `SCAVIO_REDDIT_SEARCH`, `SCAVIO_REDDIT_POST` |
|
|
81
|
+
| TikTok | `SCAVIO_TIKTOK_PROFILE`, `SCAVIO_TIKTOK_USER_POSTS`, `SCAVIO_TIKTOK_VIDEO`, `SCAVIO_TIKTOK_VIDEO_COMMENTS`, `SCAVIO_TIKTOK_COMMENT_REPLIES`, `SCAVIO_TIKTOK_SEARCH_VIDEOS`, `SCAVIO_TIKTOK_SEARCH_USERS`, `SCAVIO_TIKTOK_HASHTAG`, `SCAVIO_TIKTOK_HASHTAG_VIDEOS`, `SCAVIO_TIKTOK_USER_FOLLOWERS`, `SCAVIO_TIKTOK_USER_FOLLOWINGS` |
|
|
82
|
+
| Instagram | `SCAVIO_INSTAGRAM_PROFILE`, `SCAVIO_INSTAGRAM_USER_POSTS`, `SCAVIO_INSTAGRAM_USER_REELS`, `SCAVIO_INSTAGRAM_USER_TAGGED`, `SCAVIO_INSTAGRAM_USER_STORIES`, `SCAVIO_INSTAGRAM_POST`, `SCAVIO_INSTAGRAM_POST_COMMENTS`, `SCAVIO_INSTAGRAM_COMMENT_REPLIES`, `SCAVIO_INSTAGRAM_SEARCH_USERS`, `SCAVIO_INSTAGRAM_SEARCH_HASHTAGS`, `SCAVIO_INSTAGRAM_USER_FOLLOWERS`, `SCAVIO_INSTAGRAM_USER_FOLLOWINGS` |
|
|
83
|
+
|
|
84
|
+
## Credits
|
|
85
|
+
|
|
86
|
+
Most endpoints cost 1 credit. Reddit and Instagram cost 2 credits each. Google costs 2 credits unless `light_request=true` (1 credit). See [scavio.dev/docs](https://scavio.dev/docs).
|
|
87
|
+
|
|
88
|
+
## Links
|
|
89
|
+
|
|
90
|
+
- Scavio: https://scavio.dev
|
|
91
|
+
- Docs: https://scavio.dev/docs
|
|
92
|
+
- Dashboard: https://dashboard.scavio.dev
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# composio-scavio
|
|
2
|
+
|
|
3
|
+
[Scavio](https://scavio.dev) real-time search tools for [Composio](https://composio.dev).
|
|
4
|
+
|
|
5
|
+
Scavio is a single Search API over Google, YouTube, Amazon, Walmart, Reddit, TikTok, and Instagram. This package exposes those endpoints as a Composio custom toolkit so your agents can pull structured, up-to-date results across any Composio-supported framework (OpenAI, Anthropic, LangChain, CrewAI, and more).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install composio-scavio
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Get a Scavio API key from the [Scavio Dashboard](https://dashboard.scavio.dev) (new accounts get 50 free credits to start, no credit card). Set it as an environment variable or pass it directly.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export SCAVIO_API_KEY=sk_...
|
|
19
|
+
export COMPOSIO_API_KEY=... # from https://app.composio.dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from composio import Composio
|
|
26
|
+
from composio_scavio import build_scavio_toolkit
|
|
27
|
+
|
|
28
|
+
composio = Composio()
|
|
29
|
+
|
|
30
|
+
# Build the toolkit; expose only the providers you need.
|
|
31
|
+
scavio = build_scavio_toolkit(
|
|
32
|
+
api_key="sk_...", # or rely on SCAVIO_API_KEY
|
|
33
|
+
enable_google=True,
|
|
34
|
+
enable_amazon=True,
|
|
35
|
+
enable_tiktok=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
session = composio.create(
|
|
39
|
+
user_id="user_1",
|
|
40
|
+
experimental={"custom_toolkits": [scavio]},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = session.tools.execute(
|
|
44
|
+
"SCAVIO_GOOGLE_SEARCH",
|
|
45
|
+
arguments={"query": "best search API for AI agents", "light_request": True},
|
|
46
|
+
)
|
|
47
|
+
print(result)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pass `all=True` to register every tool regardless of the individual flags.
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
All tools are namespaced under the `SCAVIO` toolkit. Each provider is gated by an `enable_*` flag.
|
|
55
|
+
|
|
56
|
+
| Provider | Tools |
|
|
57
|
+
|----------|-------|
|
|
58
|
+
| Google | `SCAVIO_GOOGLE_SEARCH` |
|
|
59
|
+
| Amazon | `SCAVIO_AMAZON_SEARCH`, `SCAVIO_AMAZON_PRODUCT` |
|
|
60
|
+
| Walmart | `SCAVIO_WALMART_SEARCH`, `SCAVIO_WALMART_PRODUCT` |
|
|
61
|
+
| YouTube | `SCAVIO_YOUTUBE_SEARCH`, `SCAVIO_YOUTUBE_METADATA` |
|
|
62
|
+
| Reddit | `SCAVIO_REDDIT_SEARCH`, `SCAVIO_REDDIT_POST` |
|
|
63
|
+
| TikTok | `SCAVIO_TIKTOK_PROFILE`, `SCAVIO_TIKTOK_USER_POSTS`, `SCAVIO_TIKTOK_VIDEO`, `SCAVIO_TIKTOK_VIDEO_COMMENTS`, `SCAVIO_TIKTOK_COMMENT_REPLIES`, `SCAVIO_TIKTOK_SEARCH_VIDEOS`, `SCAVIO_TIKTOK_SEARCH_USERS`, `SCAVIO_TIKTOK_HASHTAG`, `SCAVIO_TIKTOK_HASHTAG_VIDEOS`, `SCAVIO_TIKTOK_USER_FOLLOWERS`, `SCAVIO_TIKTOK_USER_FOLLOWINGS` |
|
|
64
|
+
| Instagram | `SCAVIO_INSTAGRAM_PROFILE`, `SCAVIO_INSTAGRAM_USER_POSTS`, `SCAVIO_INSTAGRAM_USER_REELS`, `SCAVIO_INSTAGRAM_USER_TAGGED`, `SCAVIO_INSTAGRAM_USER_STORIES`, `SCAVIO_INSTAGRAM_POST`, `SCAVIO_INSTAGRAM_POST_COMMENTS`, `SCAVIO_INSTAGRAM_COMMENT_REPLIES`, `SCAVIO_INSTAGRAM_SEARCH_USERS`, `SCAVIO_INSTAGRAM_SEARCH_HASHTAGS`, `SCAVIO_INSTAGRAM_USER_FOLLOWERS`, `SCAVIO_INSTAGRAM_USER_FOLLOWINGS` |
|
|
65
|
+
|
|
66
|
+
## Credits
|
|
67
|
+
|
|
68
|
+
Most endpoints cost 1 credit. Reddit and Instagram cost 2 credits each. Google costs 2 credits unless `light_request=true` (1 credit). See [scavio.dev/docs](https://scavio.dev/docs).
|
|
69
|
+
|
|
70
|
+
## Links
|
|
71
|
+
|
|
72
|
+
- Scavio: https://scavio.dev
|
|
73
|
+
- Docs: https://scavio.dev/docs
|
|
74
|
+
- Dashboard: https://dashboard.scavio.dev
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Scavio tools for Composio.
|
|
2
|
+
|
|
3
|
+
Exposes the Scavio search API (Google, YouTube, Amazon, Walmart, Reddit, TikTok,
|
|
4
|
+
Instagram) as a Composio custom toolkit. Build the toolkit with
|
|
5
|
+
``build_scavio_toolkit()`` and bind it to a session::
|
|
6
|
+
|
|
7
|
+
from composio import Composio
|
|
8
|
+
from composio_scavio import build_scavio_toolkit
|
|
9
|
+
|
|
10
|
+
composio = Composio()
|
|
11
|
+
scavio = build_scavio_toolkit(api_key="sk_...") # or set SCAVIO_API_KEY
|
|
12
|
+
session = composio.create(
|
|
13
|
+
user_id="user_1",
|
|
14
|
+
experimental={"custom_toolkits": [scavio]},
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
Each provider is gated by an ``enable_*`` flag so an agent only sees the tools it
|
|
18
|
+
needs. Tools return the raw Scavio JSON response as a dict.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any, Callable, Dict, Optional
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from composio import ExperimentalToolkit
|
|
28
|
+
except ImportError as exc: # pragma: no cover
|
|
29
|
+
raise ImportError(
|
|
30
|
+
"`composio` not installed. Please install using `pip install composio`"
|
|
31
|
+
) from exc
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from scavio import ScavioClient
|
|
35
|
+
except ImportError as exc: # pragma: no cover
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"`scavio` not installed. Please install using `pip install scavio`"
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --------------------------------------------------------------------------- #
|
|
42
|
+
# Input schemas (one per tool). Field descriptions become the tool arg schema.
|
|
43
|
+
# --------------------------------------------------------------------------- #
|
|
44
|
+
|
|
45
|
+
# Google
|
|
46
|
+
class GoogleSearchInput(BaseModel):
|
|
47
|
+
query: str = Field(description="The search query.")
|
|
48
|
+
country_code: Optional[str] = Field(None, description="Two-letter country code, e.g. 'us'.")
|
|
49
|
+
language: Optional[str] = Field(None, description="Two-letter language code, e.g. 'en'.")
|
|
50
|
+
page: Optional[int] = Field(None, description="Result page number (1-based).")
|
|
51
|
+
search_type: Optional[str] = Field(None, description="Search vertical, e.g. 'search', 'news', 'images'.")
|
|
52
|
+
device: Optional[str] = Field(None, description="Device profile: 'desktop' or 'mobile'.")
|
|
53
|
+
nfpr: Optional[bool] = Field(None, description="Disable auto-correction of the query when true.")
|
|
54
|
+
light_request: Optional[bool] = Field(None, description="Cheaper, lighter response (1 credit instead of 2) when true.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Amazon
|
|
58
|
+
class AmazonSearchInput(BaseModel):
|
|
59
|
+
query: str = Field(description="The product search query.")
|
|
60
|
+
domain: Optional[str] = Field(None, description="Amazon domain, e.g. 'amazon.com'.")
|
|
61
|
+
country: Optional[str] = Field(None, description="Two-letter country code.")
|
|
62
|
+
language: Optional[str] = Field(None, description="Two-letter language code.")
|
|
63
|
+
currency: Optional[str] = Field(None, description="Currency code, e.g. 'USD'.")
|
|
64
|
+
device: Optional[str] = Field(None, description="Device profile: 'desktop' or 'mobile'.")
|
|
65
|
+
sort_by: Optional[str] = Field(None, description="Sort order for results.")
|
|
66
|
+
start_page: Optional[int] = Field(None, description="First page to return.")
|
|
67
|
+
pages: Optional[int] = Field(None, description="Number of pages to return.")
|
|
68
|
+
category_id: Optional[str] = Field(None, description="Restrict to an Amazon category id.")
|
|
69
|
+
merchant_id: Optional[str] = Field(None, description="Restrict to a merchant id.")
|
|
70
|
+
zip_code: Optional[str] = Field(None, description="Delivery ZIP/postal code.")
|
|
71
|
+
autoselect_variant: Optional[bool] = Field(None, description="Auto-select the best product variant when true.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AmazonProductInput(BaseModel):
|
|
75
|
+
asin: str = Field(description="Amazon Standard Identification Number (ASIN) of the product.")
|
|
76
|
+
domain: Optional[str] = Field(None, description="Amazon domain, e.g. 'amazon.com'.")
|
|
77
|
+
country: Optional[str] = Field(None, description="Two-letter country code.")
|
|
78
|
+
language: Optional[str] = Field(None, description="Two-letter language code.")
|
|
79
|
+
currency: Optional[str] = Field(None, description="Currency code, e.g. 'USD'.")
|
|
80
|
+
device: Optional[str] = Field(None, description="Device profile: 'desktop' or 'mobile'.")
|
|
81
|
+
zip_code: Optional[str] = Field(None, description="Delivery ZIP/postal code.")
|
|
82
|
+
autoselect_variant: Optional[bool] = Field(None, description="Auto-select the best product variant when true.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Walmart
|
|
86
|
+
class WalmartSearchInput(BaseModel):
|
|
87
|
+
query: str = Field(description="The product search query.")
|
|
88
|
+
domain: Optional[str] = Field(None, description="Walmart domain.")
|
|
89
|
+
device: Optional[str] = Field(None, description="Device profile: 'desktop' or 'mobile'.")
|
|
90
|
+
sort_by: Optional[str] = Field(None, description="Sort order for results.")
|
|
91
|
+
start_page: Optional[int] = Field(None, description="First page to return.")
|
|
92
|
+
min_price: Optional[int] = Field(None, description="Minimum price filter.")
|
|
93
|
+
max_price: Optional[int] = Field(None, description="Maximum price filter.")
|
|
94
|
+
fulfillment_speed: Optional[str] = Field(None, description="Fulfillment speed filter.")
|
|
95
|
+
fulfillment_type: Optional[str] = Field(None, description="Fulfillment type filter.")
|
|
96
|
+
delivery_zip: Optional[str] = Field(None, description="Delivery ZIP/postal code.")
|
|
97
|
+
store_id: Optional[str] = Field(None, description="Restrict to a store id.")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WalmartProductInput(BaseModel):
|
|
101
|
+
product_id: str = Field(description="Walmart product id.")
|
|
102
|
+
domain: Optional[str] = Field(None, description="Walmart domain.")
|
|
103
|
+
device: Optional[str] = Field(None, description="Device profile: 'desktop' or 'mobile'.")
|
|
104
|
+
delivery_zip: Optional[str] = Field(None, description="Delivery ZIP/postal code.")
|
|
105
|
+
store_id: Optional[str] = Field(None, description="Restrict to a store id.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# YouTube
|
|
109
|
+
class YouTubeSearchInput(BaseModel):
|
|
110
|
+
query: str = Field(description="The video search query.")
|
|
111
|
+
upload_date: Optional[str] = Field(None, description="Upload date filter, e.g. 'today', 'week', 'month'.")
|
|
112
|
+
type: Optional[str] = Field(None, description="Result type, e.g. 'video', 'channel', 'playlist'.")
|
|
113
|
+
duration: Optional[str] = Field(None, description="Duration filter, e.g. 'short', 'long'.")
|
|
114
|
+
sort_by: Optional[str] = Field(None, description="Sort order for results.")
|
|
115
|
+
hd: Optional[bool] = Field(None, description="Restrict to HD videos when true.")
|
|
116
|
+
subtitles: Optional[bool] = Field(None, description="Restrict to videos with subtitles when true.")
|
|
117
|
+
creative_commons: Optional[bool] = Field(None, description="Restrict to Creative Commons videos when true.")
|
|
118
|
+
live: Optional[bool] = Field(None, description="Restrict to live videos when true.")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class YouTubeMetadataInput(BaseModel):
|
|
122
|
+
video_id: str = Field(description="YouTube video id.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Reddit
|
|
126
|
+
class RedditSearchInput(BaseModel):
|
|
127
|
+
query: str = Field(description="The Reddit search query.")
|
|
128
|
+
type: Optional[str] = Field(None, description="Search type, e.g. 'posts', 'subreddits', 'users'.")
|
|
129
|
+
sort: Optional[str] = Field(None, description="Sort order, e.g. 'relevance', 'new', 'top'.")
|
|
130
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RedditPostInput(BaseModel):
|
|
134
|
+
url: str = Field(description="Full URL of the Reddit post to fetch with its comments.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# TikTok
|
|
138
|
+
class TikTokProfileInput(BaseModel):
|
|
139
|
+
username: Optional[str] = Field(None, description="TikTok username (without @). Provide this or sec_user_id.")
|
|
140
|
+
sec_user_id: Optional[str] = Field(None, description="TikTok secUid. Provide this or username.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TikTokUserPostsInput(BaseModel):
|
|
144
|
+
sec_user_id: str = Field(description="TikTok secUid of the user.")
|
|
145
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
146
|
+
count: Optional[int] = Field(None, description="Number of posts to return.")
|
|
147
|
+
sort_type: Optional[str] = Field(None, description="Sort order for posts.")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TikTokVideoInput(BaseModel):
|
|
151
|
+
video_id: str = Field(description="TikTok video id.")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TikTokVideoCommentsInput(BaseModel):
|
|
155
|
+
video_id: str = Field(description="TikTok video id.")
|
|
156
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
157
|
+
count: Optional[int] = Field(None, description="Number of comments to return.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TikTokCommentRepliesInput(BaseModel):
|
|
161
|
+
video_id: str = Field(description="TikTok video id.")
|
|
162
|
+
comment_id: str = Field(description="Parent comment id.")
|
|
163
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
164
|
+
count: Optional[int] = Field(None, description="Number of replies to return.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TikTokSearchVideosInput(BaseModel):
|
|
168
|
+
keyword: str = Field(description="Search keyword.")
|
|
169
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
170
|
+
count: Optional[int] = Field(None, description="Number of videos to return.")
|
|
171
|
+
sort_type: Optional[str] = Field(None, description="Sort order for results.")
|
|
172
|
+
publish_time: Optional[str] = Field(None, description="Publish-time filter.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TikTokSearchUsersInput(BaseModel):
|
|
176
|
+
keyword: str = Field(description="Search keyword.")
|
|
177
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
178
|
+
count: Optional[int] = Field(None, description="Number of users to return.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TikTokHashtagInput(BaseModel):
|
|
182
|
+
hashtag_name: Optional[str] = Field(None, description="Hashtag name (without #). Provide this or hashtag_id.")
|
|
183
|
+
hashtag_id: Optional[str] = Field(None, description="Hashtag id. Provide this or hashtag_name.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TikTokHashtagVideosInput(BaseModel):
|
|
187
|
+
hashtag_id: str = Field(description="Hashtag id.")
|
|
188
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
189
|
+
count: Optional[int] = Field(None, description="Number of videos to return.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TikTokUserFollowersInput(BaseModel):
|
|
193
|
+
sec_user_id: str = Field(description="TikTok secUid of the user.")
|
|
194
|
+
count: Optional[int] = Field(None, description="Number of followers to return.")
|
|
195
|
+
page_token: Optional[str] = Field(None, description="Pagination token.")
|
|
196
|
+
min_time: Optional[int] = Field(None, description="Minimum timestamp filter.")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TikTokUserFollowingsInput(BaseModel):
|
|
200
|
+
sec_user_id: str = Field(description="TikTok secUid of the user.")
|
|
201
|
+
count: Optional[int] = Field(None, description="Number of followings to return.")
|
|
202
|
+
page_token: Optional[str] = Field(None, description="Pagination token.")
|
|
203
|
+
min_time: Optional[int] = Field(None, description="Minimum timestamp filter.")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Instagram
|
|
207
|
+
class InstagramProfileInput(BaseModel):
|
|
208
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
209
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class InstagramUserPostsInput(BaseModel):
|
|
213
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
214
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
215
|
+
count: Optional[int] = Field(None, description="Number of posts to return.")
|
|
216
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class InstagramUserReelsInput(BaseModel):
|
|
220
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
221
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
222
|
+
count: Optional[int] = Field(None, description="Number of reels to return.")
|
|
223
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class InstagramUserTaggedInput(BaseModel):
|
|
227
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
228
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
229
|
+
count: Optional[int] = Field(None, description="Number of tagged posts to return.")
|
|
230
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class InstagramUserStoriesInput(BaseModel):
|
|
234
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
235
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class InstagramPostInput(BaseModel):
|
|
239
|
+
url: Optional[str] = Field(None, description="Post URL. Provide one of url, media_id, or shortcode.")
|
|
240
|
+
media_id: Optional[str] = Field(None, description="Post media id. Provide one of url, media_id, or shortcode.")
|
|
241
|
+
shortcode: Optional[str] = Field(None, description="Post shortcode. Provide one of url, media_id, or shortcode.")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class InstagramPostCommentsInput(BaseModel):
|
|
245
|
+
shortcode: Optional[str] = Field(None, description="Post shortcode. Provide this or url.")
|
|
246
|
+
url: Optional[str] = Field(None, description="Post URL. Provide this or shortcode.")
|
|
247
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
248
|
+
sort_order: Optional[str] = Field(None, description="Comment sort order.")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class InstagramCommentRepliesInput(BaseModel):
|
|
252
|
+
media_id: str = Field(description="Post media id.")
|
|
253
|
+
comment_id: str = Field(description="Parent comment id.")
|
|
254
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class InstagramSearchUsersInput(BaseModel):
|
|
258
|
+
keyword: str = Field(description="Search keyword.")
|
|
259
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class InstagramSearchHashtagsInput(BaseModel):
|
|
263
|
+
keyword: str = Field(description="Search keyword.")
|
|
264
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class InstagramUserFollowersInput(BaseModel):
|
|
268
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
269
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
270
|
+
count: Optional[int] = Field(None, description="Number of followers to return.")
|
|
271
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class InstagramUserFollowingsInput(BaseModel):
|
|
275
|
+
username: Optional[str] = Field(None, description="Instagram username. Provide this or user_id.")
|
|
276
|
+
user_id: Optional[str] = Field(None, description="Instagram user id. Provide this or username.")
|
|
277
|
+
count: Optional[int] = Field(None, description="Number of followings to return.")
|
|
278
|
+
cursor: Optional[str] = Field(None, description="Pagination cursor.")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _run(call: Callable[[], Dict[str, Any]]) -> Dict[str, Any]:
|
|
282
|
+
"""Run a Scavio SDK call, returning its JSON dict or an ``{"error": ...}`` dict."""
|
|
283
|
+
try:
|
|
284
|
+
return call()
|
|
285
|
+
except Exception as exc: # noqa: BLE001 - surface any SDK/network error to the agent
|
|
286
|
+
return {"error": str(exc)}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def build_scavio_toolkit(
|
|
290
|
+
api_key: Optional[str] = None,
|
|
291
|
+
*,
|
|
292
|
+
enable_google: bool = True,
|
|
293
|
+
enable_amazon: bool = True,
|
|
294
|
+
enable_walmart: bool = True,
|
|
295
|
+
enable_youtube: bool = True,
|
|
296
|
+
enable_reddit: bool = True,
|
|
297
|
+
enable_tiktok: bool = True,
|
|
298
|
+
enable_instagram: bool = True,
|
|
299
|
+
all: bool = False,
|
|
300
|
+
) -> "ExperimentalToolkit":
|
|
301
|
+
"""Build a Composio custom toolkit exposing Scavio search tools.
|
|
302
|
+
|
|
303
|
+
Scavio is a single Search API over Google, YouTube, Amazon, Walmart, Reddit,
|
|
304
|
+
TikTok, and Instagram. Each provider is gated by an ``enable_*`` flag so you
|
|
305
|
+
expose only the tools your agent needs.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
api_key: Scavio API key. Falls back to the ``SCAVIO_API_KEY`` env var.
|
|
309
|
+
enable_google: Register the Google web search tool. Defaults to True.
|
|
310
|
+
enable_amazon: Register the Amazon search and product tools. Defaults to True.
|
|
311
|
+
enable_walmart: Register the Walmart search and product tools. Defaults to True.
|
|
312
|
+
enable_youtube: Register the YouTube search and metadata tools. Defaults to True.
|
|
313
|
+
enable_reddit: Register the Reddit search and post tools. Defaults to True.
|
|
314
|
+
enable_tiktok: Register the TikTok tools. Defaults to True.
|
|
315
|
+
enable_instagram: Register the Instagram tools. Defaults to True.
|
|
316
|
+
all: Register every tool, ignoring the individual flags. Defaults to False.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
An ``ExperimentalToolkit`` to pass to
|
|
320
|
+
``composio.create(..., experimental={"custom_toolkits": [toolkit]})``.
|
|
321
|
+
"""
|
|
322
|
+
key = api_key or os.getenv("SCAVIO_API_KEY")
|
|
323
|
+
client = ScavioClient(api_key=key)
|
|
324
|
+
|
|
325
|
+
toolkit = ExperimentalToolkit(
|
|
326
|
+
slug="SCAVIO",
|
|
327
|
+
name="Scavio",
|
|
328
|
+
description=(
|
|
329
|
+
"Real-time structured search over Google, YouTube, Amazon, Walmart, "
|
|
330
|
+
"Reddit, TikTok, and Instagram."
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def dump(model: BaseModel) -> Dict[str, Any]:
|
|
335
|
+
return model.model_dump(exclude_none=True)
|
|
336
|
+
|
|
337
|
+
if all or enable_google:
|
|
338
|
+
|
|
339
|
+
@toolkit.tool()
|
|
340
|
+
def scavio_google_search(input: GoogleSearchInput, ctx: Any = None) -> dict:
|
|
341
|
+
"""Search Google for real-time web results (organic, knowledge graph, news, and more)."""
|
|
342
|
+
return _run(lambda: client.google.search(**dump(input)))
|
|
343
|
+
|
|
344
|
+
if all or enable_amazon:
|
|
345
|
+
|
|
346
|
+
@toolkit.tool()
|
|
347
|
+
def scavio_amazon_search(input: AmazonSearchInput, ctx: Any = None) -> dict:
|
|
348
|
+
"""Search Amazon for products matching a query."""
|
|
349
|
+
return _run(lambda: client.amazon.search(**dump(input)))
|
|
350
|
+
|
|
351
|
+
@toolkit.tool()
|
|
352
|
+
def scavio_amazon_product(input: AmazonProductInput, ctx: Any = None) -> dict:
|
|
353
|
+
"""Fetch full Amazon product details by ASIN."""
|
|
354
|
+
return _run(lambda: client.amazon.product(**dump(input)))
|
|
355
|
+
|
|
356
|
+
if all or enable_walmart:
|
|
357
|
+
|
|
358
|
+
@toolkit.tool()
|
|
359
|
+
def scavio_walmart_search(input: WalmartSearchInput, ctx: Any = None) -> dict:
|
|
360
|
+
"""Search Walmart for products matching a query."""
|
|
361
|
+
return _run(lambda: client.walmart.search(**dump(input)))
|
|
362
|
+
|
|
363
|
+
@toolkit.tool()
|
|
364
|
+
def scavio_walmart_product(input: WalmartProductInput, ctx: Any = None) -> dict:
|
|
365
|
+
"""Fetch full Walmart product details by product id."""
|
|
366
|
+
return _run(lambda: client.walmart.product(**dump(input)))
|
|
367
|
+
|
|
368
|
+
if all or enable_youtube:
|
|
369
|
+
|
|
370
|
+
@toolkit.tool()
|
|
371
|
+
def scavio_youtube_search(input: YouTubeSearchInput, ctx: Any = None) -> dict:
|
|
372
|
+
"""Search YouTube for videos, channels, or playlists."""
|
|
373
|
+
return _run(lambda: client.youtube.search(**dump(input)))
|
|
374
|
+
|
|
375
|
+
@toolkit.tool()
|
|
376
|
+
def scavio_youtube_metadata(input: YouTubeMetadataInput, ctx: Any = None) -> dict:
|
|
377
|
+
"""Fetch metadata for a YouTube video by id."""
|
|
378
|
+
return _run(lambda: client.youtube.metadata(**dump(input)))
|
|
379
|
+
|
|
380
|
+
if all or enable_reddit:
|
|
381
|
+
|
|
382
|
+
@toolkit.tool()
|
|
383
|
+
def scavio_reddit_search(input: RedditSearchInput, ctx: Any = None) -> dict:
|
|
384
|
+
"""Search Reddit posts, subreddits, or users."""
|
|
385
|
+
return _run(lambda: client.reddit.search(**dump(input)))
|
|
386
|
+
|
|
387
|
+
@toolkit.tool()
|
|
388
|
+
def scavio_reddit_post(input: RedditPostInput, ctx: Any = None) -> dict:
|
|
389
|
+
"""Fetch a Reddit post and its comment thread by URL."""
|
|
390
|
+
return _run(lambda: client.reddit.post(**dump(input)))
|
|
391
|
+
|
|
392
|
+
if all or enable_tiktok:
|
|
393
|
+
|
|
394
|
+
@toolkit.tool()
|
|
395
|
+
def scavio_tiktok_profile(input: TikTokProfileInput, ctx: Any = None) -> dict:
|
|
396
|
+
"""Fetch a TikTok user profile by username or secUid."""
|
|
397
|
+
return _run(lambda: client.tiktok.profile(**dump(input)))
|
|
398
|
+
|
|
399
|
+
@toolkit.tool()
|
|
400
|
+
def scavio_tiktok_user_posts(input: TikTokUserPostsInput, ctx: Any = None) -> dict:
|
|
401
|
+
"""List a TikTok user's posts by secUid."""
|
|
402
|
+
return _run(lambda: client.tiktok.user_posts(**dump(input)))
|
|
403
|
+
|
|
404
|
+
@toolkit.tool()
|
|
405
|
+
def scavio_tiktok_video(input: TikTokVideoInput, ctx: Any = None) -> dict:
|
|
406
|
+
"""Fetch a TikTok video by id."""
|
|
407
|
+
return _run(lambda: client.tiktok.video(**dump(input)))
|
|
408
|
+
|
|
409
|
+
@toolkit.tool()
|
|
410
|
+
def scavio_tiktok_video_comments(input: TikTokVideoCommentsInput, ctx: Any = None) -> dict:
|
|
411
|
+
"""List comments on a TikTok video."""
|
|
412
|
+
return _run(lambda: client.tiktok.video_comments(**dump(input)))
|
|
413
|
+
|
|
414
|
+
@toolkit.tool()
|
|
415
|
+
def scavio_tiktok_comment_replies(input: TikTokCommentRepliesInput, ctx: Any = None) -> dict:
|
|
416
|
+
"""List replies to a TikTok video comment."""
|
|
417
|
+
return _run(lambda: client.tiktok.comment_replies(**dump(input)))
|
|
418
|
+
|
|
419
|
+
@toolkit.tool()
|
|
420
|
+
def scavio_tiktok_search_videos(input: TikTokSearchVideosInput, ctx: Any = None) -> dict:
|
|
421
|
+
"""Search TikTok videos by keyword."""
|
|
422
|
+
return _run(lambda: client.tiktok.search_videos(**dump(input)))
|
|
423
|
+
|
|
424
|
+
@toolkit.tool()
|
|
425
|
+
def scavio_tiktok_search_users(input: TikTokSearchUsersInput, ctx: Any = None) -> dict:
|
|
426
|
+
"""Search TikTok users by keyword."""
|
|
427
|
+
return _run(lambda: client.tiktok.search_users(**dump(input)))
|
|
428
|
+
|
|
429
|
+
@toolkit.tool()
|
|
430
|
+
def scavio_tiktok_hashtag(input: TikTokHashtagInput, ctx: Any = None) -> dict:
|
|
431
|
+
"""Fetch a TikTok hashtag by name or id."""
|
|
432
|
+
return _run(lambda: client.tiktok.hashtag(**dump(input)))
|
|
433
|
+
|
|
434
|
+
@toolkit.tool()
|
|
435
|
+
def scavio_tiktok_hashtag_videos(input: TikTokHashtagVideosInput, ctx: Any = None) -> dict:
|
|
436
|
+
"""List videos for a TikTok hashtag by id."""
|
|
437
|
+
return _run(lambda: client.tiktok.hashtag_videos(**dump(input)))
|
|
438
|
+
|
|
439
|
+
@toolkit.tool()
|
|
440
|
+
def scavio_tiktok_user_followers(input: TikTokUserFollowersInput, ctx: Any = None) -> dict:
|
|
441
|
+
"""List a TikTok user's followers by secUid."""
|
|
442
|
+
return _run(lambda: client.tiktok.user_followers(**dump(input)))
|
|
443
|
+
|
|
444
|
+
@toolkit.tool()
|
|
445
|
+
def scavio_tiktok_user_followings(input: TikTokUserFollowingsInput, ctx: Any = None) -> dict:
|
|
446
|
+
"""List the accounts a TikTok user follows, by secUid."""
|
|
447
|
+
return _run(lambda: client.tiktok.user_followings(**dump(input)))
|
|
448
|
+
|
|
449
|
+
if all or enable_instagram:
|
|
450
|
+
|
|
451
|
+
@toolkit.tool()
|
|
452
|
+
def scavio_instagram_profile(input: InstagramProfileInput, ctx: Any = None) -> dict:
|
|
453
|
+
"""Fetch an Instagram profile by username or user id."""
|
|
454
|
+
return _run(lambda: client.instagram.profile(**dump(input)))
|
|
455
|
+
|
|
456
|
+
@toolkit.tool()
|
|
457
|
+
def scavio_instagram_user_posts(input: InstagramUserPostsInput, ctx: Any = None) -> dict:
|
|
458
|
+
"""List an Instagram user's posts."""
|
|
459
|
+
return _run(lambda: client.instagram.user_posts(**dump(input)))
|
|
460
|
+
|
|
461
|
+
@toolkit.tool()
|
|
462
|
+
def scavio_instagram_user_reels(input: InstagramUserReelsInput, ctx: Any = None) -> dict:
|
|
463
|
+
"""List an Instagram user's reels."""
|
|
464
|
+
return _run(lambda: client.instagram.user_reels(**dump(input)))
|
|
465
|
+
|
|
466
|
+
@toolkit.tool()
|
|
467
|
+
def scavio_instagram_user_tagged(input: InstagramUserTaggedInput, ctx: Any = None) -> dict:
|
|
468
|
+
"""List posts an Instagram user is tagged in."""
|
|
469
|
+
return _run(lambda: client.instagram.user_tagged(**dump(input)))
|
|
470
|
+
|
|
471
|
+
@toolkit.tool()
|
|
472
|
+
def scavio_instagram_user_stories(input: InstagramUserStoriesInput, ctx: Any = None) -> dict:
|
|
473
|
+
"""Fetch an Instagram user's current stories."""
|
|
474
|
+
return _run(lambda: client.instagram.user_stories(**dump(input)))
|
|
475
|
+
|
|
476
|
+
@toolkit.tool()
|
|
477
|
+
def scavio_instagram_post(input: InstagramPostInput, ctx: Any = None) -> dict:
|
|
478
|
+
"""Fetch an Instagram post by URL, media id, or shortcode."""
|
|
479
|
+
return _run(lambda: client.instagram.post(**dump(input)))
|
|
480
|
+
|
|
481
|
+
@toolkit.tool()
|
|
482
|
+
def scavio_instagram_post_comments(input: InstagramPostCommentsInput, ctx: Any = None) -> dict:
|
|
483
|
+
"""List comments on an Instagram post by shortcode or URL."""
|
|
484
|
+
return _run(lambda: client.instagram.post_comments(**dump(input)))
|
|
485
|
+
|
|
486
|
+
@toolkit.tool()
|
|
487
|
+
def scavio_instagram_comment_replies(input: InstagramCommentRepliesInput, ctx: Any = None) -> dict:
|
|
488
|
+
"""List replies to an Instagram post comment."""
|
|
489
|
+
return _run(lambda: client.instagram.comment_replies(**dump(input)))
|
|
490
|
+
|
|
491
|
+
@toolkit.tool()
|
|
492
|
+
def scavio_instagram_search_users(input: InstagramSearchUsersInput, ctx: Any = None) -> dict:
|
|
493
|
+
"""Search Instagram users by keyword."""
|
|
494
|
+
return _run(lambda: client.instagram.search_users(**dump(input)))
|
|
495
|
+
|
|
496
|
+
@toolkit.tool()
|
|
497
|
+
def scavio_instagram_search_hashtags(input: InstagramSearchHashtagsInput, ctx: Any = None) -> dict:
|
|
498
|
+
"""Search Instagram hashtags by keyword."""
|
|
499
|
+
return _run(lambda: client.instagram.search_hashtags(**dump(input)))
|
|
500
|
+
|
|
501
|
+
@toolkit.tool()
|
|
502
|
+
def scavio_instagram_user_followers(input: InstagramUserFollowersInput, ctx: Any = None) -> dict:
|
|
503
|
+
"""List an Instagram user's followers."""
|
|
504
|
+
return _run(lambda: client.instagram.user_followers(**dump(input)))
|
|
505
|
+
|
|
506
|
+
@toolkit.tool()
|
|
507
|
+
def scavio_instagram_user_followings(input: InstagramUserFollowingsInput, ctx: Any = None) -> dict:
|
|
508
|
+
"""List the accounts an Instagram user follows."""
|
|
509
|
+
return _run(lambda: client.instagram.user_followings(**dump(input)))
|
|
510
|
+
|
|
511
|
+
return toolkit
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Run Scavio tools inside a Composio session.
|
|
2
|
+
|
|
3
|
+
Prerequisites:
|
|
4
|
+
pip install composio-scavio
|
|
5
|
+
export SCAVIO_API_KEY=sk_... # from https://dashboard.scavio.dev
|
|
6
|
+
export COMPOSIO_API_KEY=... # from https://app.composio.dev
|
|
7
|
+
|
|
8
|
+
This builds the Scavio custom toolkit, binds it to a Composio session, and runs a
|
|
9
|
+
Google search tool directly. Wire the same tools into any Composio-supported agent
|
|
10
|
+
framework (OpenAI, Anthropic, LangChain, CrewAI, ...).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from composio import Composio
|
|
16
|
+
|
|
17
|
+
from composio_scavio import build_scavio_toolkit
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
if not os.getenv("SCAVIO_API_KEY"):
|
|
22
|
+
raise SystemExit("Set SCAVIO_API_KEY first (https://dashboard.scavio.dev).")
|
|
23
|
+
|
|
24
|
+
composio = Composio()
|
|
25
|
+
|
|
26
|
+
# Expose only the providers you need; here just Google.
|
|
27
|
+
scavio = build_scavio_toolkit(
|
|
28
|
+
enable_google=True,
|
|
29
|
+
enable_amazon=False,
|
|
30
|
+
enable_walmart=False,
|
|
31
|
+
enable_youtube=False,
|
|
32
|
+
enable_reddit=False,
|
|
33
|
+
enable_tiktok=False,
|
|
34
|
+
enable_instagram=False,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
session = composio.create(
|
|
38
|
+
user_id="cookbook-user",
|
|
39
|
+
experimental={"custom_toolkits": [scavio]},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
result = session.tools.execute(
|
|
43
|
+
"SCAVIO_GOOGLE_SEARCH",
|
|
44
|
+
arguments={"query": "best structured search API for AI agents", "light_request": True},
|
|
45
|
+
)
|
|
46
|
+
print(result)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "composio-scavio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scavio real-time search tools for Composio (Google, YouTube, Amazon, Walmart, Reddit, TikTok, Instagram)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Scavio", email = "scavio.dev@gmail.com" }]
|
|
13
|
+
keywords = ["composio", "scavio", "search", "agents", "tools", "serp", "google"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"composio>=0.17.0",
|
|
16
|
+
"scavio>=0.2.1",
|
|
17
|
+
"pydantic>=2.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://scavio.dev"
|
|
22
|
+
Repository = "https://github.com/scavio-ai/composio-scavio"
|
|
23
|
+
Documentation = "https://scavio.dev/docs"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=7.0"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["composio_scavio"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Tests for composio-scavio. The Scavio SDK client is mocked, so no key or network is used."""
|
|
2
|
+
|
|
3
|
+
import composio_scavio.tools as tools_mod
|
|
4
|
+
from composio_scavio import build_scavio_toolkit
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _Recorder:
|
|
8
|
+
"""Stands in for a Scavio SDK namespace; records calls and returns a canned dict."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, calls):
|
|
11
|
+
self._calls = calls
|
|
12
|
+
|
|
13
|
+
def __getattr__(self, method):
|
|
14
|
+
def _call(**kwargs):
|
|
15
|
+
self._calls.append((method, kwargs))
|
|
16
|
+
return {"ok": True, "method": method, "kwargs": kwargs}
|
|
17
|
+
|
|
18
|
+
return _call
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _FakeClient:
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
self.calls = []
|
|
24
|
+
for ns in ("google", "amazon", "walmart", "youtube", "reddit", "tiktok", "instagram"):
|
|
25
|
+
setattr(self, ns, _Recorder(self.calls))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build(monkeypatch, **kwargs):
|
|
29
|
+
monkeypatch.setattr(tools_mod, "ScavioClient", _FakeClient)
|
|
30
|
+
return build_scavio_toolkit(api_key="test", **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_all_tools_register(monkeypatch):
|
|
34
|
+
toolkit = _build(monkeypatch, all=True)
|
|
35
|
+
slugs = [t.slug for t in toolkit.tools]
|
|
36
|
+
assert len(slugs) == 32, slugs
|
|
37
|
+
assert len(set(slugs)) == len(slugs), "slugs must be unique"
|
|
38
|
+
assert all(s.startswith("SCAVIO_") for s in slugs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_provider_gating(monkeypatch):
|
|
42
|
+
toolkit = _build(
|
|
43
|
+
monkeypatch,
|
|
44
|
+
enable_google=False,
|
|
45
|
+
enable_amazon=False,
|
|
46
|
+
enable_walmart=False,
|
|
47
|
+
enable_youtube=False,
|
|
48
|
+
enable_reddit=True,
|
|
49
|
+
enable_tiktok=False,
|
|
50
|
+
enable_instagram=False,
|
|
51
|
+
)
|
|
52
|
+
slugs = {t.slug for t in toolkit.tools}
|
|
53
|
+
assert slugs == {"SCAVIO_REDDIT_SEARCH", "SCAVIO_REDDIT_POST"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_all_overrides_flags(monkeypatch):
|
|
57
|
+
toolkit = _build(monkeypatch, all=True, enable_reddit=False, enable_tiktok=False)
|
|
58
|
+
slugs = {t.slug for t in toolkit.tools}
|
|
59
|
+
assert "SCAVIO_REDDIT_SEARCH" in slugs
|
|
60
|
+
assert "SCAVIO_TIKTOK_PROFILE" in slugs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_execution_forwards_params_and_drops_none(monkeypatch):
|
|
64
|
+
toolkit = _build(monkeypatch, enable_google=True, enable_amazon=False, enable_walmart=False,
|
|
65
|
+
enable_youtube=False, enable_reddit=False, enable_tiktok=False, enable_instagram=False)
|
|
66
|
+
tool = next(t for t in toolkit.tools if t.slug == "SCAVIO_GOOGLE_SEARCH")
|
|
67
|
+
out = tool.execute(tool.input_params(query="ai agents", light_request=True), None)
|
|
68
|
+
assert out["ok"] is True
|
|
69
|
+
assert out["method"] == "search"
|
|
70
|
+
# None-valued optional fields must not be forwarded to the SDK
|
|
71
|
+
assert out["kwargs"] == {"query": "ai agents", "light_request": True}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_amazon_product_uses_asin(monkeypatch):
|
|
75
|
+
toolkit = _build(monkeypatch, enable_amazon=True, enable_google=False, enable_walmart=False,
|
|
76
|
+
enable_youtube=False, enable_reddit=False, enable_tiktok=False, enable_instagram=False)
|
|
77
|
+
tool = next(t for t in toolkit.tools if t.slug == "SCAVIO_AMAZON_PRODUCT")
|
|
78
|
+
out = tool.execute(tool.input_params(asin="B000000000"), None)
|
|
79
|
+
assert out["method"] == "product"
|
|
80
|
+
assert out["kwargs"] == {"asin": "B000000000"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_error_is_returned_as_dict(monkeypatch):
|
|
84
|
+
def boom(**kwargs):
|
|
85
|
+
raise RuntimeError("network down")
|
|
86
|
+
|
|
87
|
+
class FailingClient(_FakeClient):
|
|
88
|
+
def __init__(self, *args, **kwargs):
|
|
89
|
+
super().__init__(*args, **kwargs)
|
|
90
|
+
self.google.search = boom # type: ignore[attr-defined]
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(tools_mod, "ScavioClient", FailingClient)
|
|
93
|
+
toolkit = build_scavio_toolkit(api_key="test", enable_google=True, enable_amazon=False,
|
|
94
|
+
enable_walmart=False, enable_youtube=False, enable_reddit=False,
|
|
95
|
+
enable_tiktok=False, enable_instagram=False)
|
|
96
|
+
tool = next(t for t in toolkit.tools if t.slug == "SCAVIO_GOOGLE_SEARCH")
|
|
97
|
+
out = tool.execute(tool.input_params(query="x"), None)
|
|
98
|
+
assert out == {"error": "network down"}
|