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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ node_modules/
8
+ *.tsbuildinfo
@@ -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,6 @@
1
+ """Scavio toolkit for Composio."""
2
+
3
+ from .tools import build_scavio_toolkit
4
+
5
+ __all__ = ["build_scavio_toolkit"]
6
+ __version__ = "0.1.0"
@@ -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"}