rusticai-serpapi 0.0.1__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,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: rusticai-serpapi
3
+ Version: 0.0.1
4
+ Summary: Rustic AI module supporting Search through SerpApi
5
+ Home-page: https://www.rustic.ai/
6
+ License: Apache-2.0
7
+ Author: Dragonscale Industries Inc.
8
+ Author-email: dev@dragonscale.ai
9
+ Requires-Python: >=3.12,<4.0
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Provides-Extra: test
15
+ Requires-Dist: rusticai-core (>=0.0.5,<0.1.0)
16
+ Requires-Dist: serpapi (>=0.1.5,<0.2.0)
17
+ Project-URL: Repository, https://github.com/rustic-ai/python-framework
18
+ Project-URL: Rustic AI Core, https://pypi.org/project/rusticai-core/
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Rustic AI SerpApi
22
+
23
+ This module provides an agent that can be used to Search the web using [SerpApi](https://serpapi.com/)
24
+
25
+
26
+ ## Installing
27
+
28
+ ```shell
29
+ pip install rusticai-serpapi
30
+ ```
31
+ **Note:** It depends on [rusticai-core](https://pypi.org/project/rusticai-core/)
32
+
33
+ ## Building from Source
34
+
35
+ ```shell
36
+ poetry install --with dev
37
+ poetry build
38
+ ```
@@ -0,0 +1,18 @@
1
+ # Rustic AI SerpApi
2
+
3
+ This module provides an agent that can be used to Search the web using [SerpApi](https://serpapi.com/)
4
+
5
+
6
+ ## Installing
7
+
8
+ ```shell
9
+ pip install rusticai-serpapi
10
+ ```
11
+ **Note:** It depends on [rusticai-core](https://pypi.org/project/rusticai-core/)
12
+
13
+ ## Building from Source
14
+
15
+ ```shell
16
+ poetry install --with dev
17
+ poetry build
18
+ ```
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "rusticai-serpapi"
7
+ version = "0.0.1"
8
+ description = "Rustic AI module supporting Search through SerpApi"
9
+ authors = ["Dragonscale Industries Inc. <dev@dragonscale.ai>"]
10
+ license = "Apache-2.0"
11
+ readme = "README.md"
12
+ homepage = "https://www.rustic.ai/"
13
+ repository = "https://github.com/rustic-ai/python-framework"
14
+ packages = [{ include = "rustic_ai", from = "src" }]
15
+
16
+ [tool.poetry.urls]
17
+ "Rustic AI Core" = "https://pypi.org/project/rusticai-core/"
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "^3.12"
21
+ rusticai-core = { version = "0.0.5"}
22
+ serpapi = "^0.1.5"
23
+
24
+ [tool.poetry-monorepo.deps]
25
+
26
+
27
+ [tool.poetry.group.dev]
28
+ optional = true
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ pytest = "^8.3.4"
32
+ black = { version = "^24.2.0", extras = ["jupyter"] }
33
+ flake8 = "^7.1.2"
34
+ tox = "^4.24.1"
35
+ isort = "^6.0.0"
36
+ mypy = "^1.15.0"
37
+ rusticai-testing = { version = "0.0.1"}
38
+
39
+ [tool.poetry.extras]
40
+ test = ["pytest", "rusticai-testing"]
41
+
42
+ [tool.black]
43
+ line-length = 120
44
+ target-version = ['py312']
45
+ include = '\.pyi?$'
46
+ exclude = '''
47
+ /(
48
+ \.git
49
+ | \.mypy_cache
50
+ | \.tox
51
+ | \.venv
52
+ | _build
53
+ | buck-out
54
+ | build
55
+ | dist
56
+ )/
57
+ '''
58
+
59
+ [tool.mypy]
60
+ python_version = "3.12"
61
+ ignore_missing_imports = true
62
+ check_untyped_defs = true
63
+ plugins = "pydantic.mypy"
64
+ explicit_package_bases = true
65
+
66
+
67
+ [tool.isort]
68
+ profile = "black"
69
+ known_third_party = ["pydantic"]
70
+ known_first_party = ["rustic_ai.core", "rustic_ai.serpapi"]
71
+ known_local_folder = ["rustic_ai.testing"]
72
+
@@ -0,0 +1,148 @@
1
+ import os
2
+ from enum import Enum
3
+ from typing import List, Optional
4
+ from urllib.parse import urlparse
5
+
6
+ import shortuuid
7
+ from pydantic import BaseModel, Field
8
+
9
+ import serpapi
10
+ from rustic_ai.core.agents.commons.media import MediaLink
11
+ from rustic_ai.core.guild import Agent, AgentMode, AgentSpec, AgentType, agent
12
+ from rustic_ai.core.messaging.core import JsonDict
13
+
14
+
15
+ class SearchEngines(Enum):
16
+ google = {"key": "q"}
17
+ bing = {"key": "q"}
18
+ baidu = {"key": "q"}
19
+ yahoo = {"key": "p"}
20
+ duckduckgo = {"key": "q"}
21
+ ebay = {"key": "_nkw"}
22
+ yandex = {"key": "text"}
23
+ home_depot = {"key": "keyword"}
24
+ google_scholar = {"key": "q"}
25
+ youtube = {"key": "search_query"}
26
+ walmart = {"key": "query"}
27
+ google_maps = {"key": "query"}
28
+ google_patents = {"key": "q"}
29
+
30
+ def get_query(self, query):
31
+ return {self.value["key"]: query}
32
+
33
+
34
+ class SERPQuery(BaseModel):
35
+ """
36
+ A class representing a search query.
37
+ """
38
+
39
+ engine: str
40
+ query: str
41
+ id: str = Field(default="")
42
+ num: Optional[int] = Field(default=12)
43
+ start: Optional[int] = Field(default=0)
44
+
45
+
46
+ class SERPResults(BaseModel):
47
+ """
48
+ A class representing a search result.
49
+ """
50
+
51
+ engine: str
52
+ query: str
53
+ count: int
54
+ id: str = Field(default="")
55
+ results: List[MediaLink]
56
+ total_results: Optional[int] = Field(default=None)
57
+
58
+
59
+ class SearchError(BaseModel):
60
+
61
+ id: str = Field(default="")
62
+ response: JsonDict
63
+
64
+
65
+ class SERPAgent(Agent):
66
+ def __init__(self, agent_spec: AgentSpec):
67
+ super().__init__(agent_spec=agent_spec, agent_type=AgentType.BOT, agent_mode=AgentMode.LOCAL)
68
+
69
+ self.serp_api_key = os.getenv("SERP_API_KEY", "")
70
+ assert self.serp_api_key, "SERP_API_KEY environment variable not set"
71
+ self.client = serpapi.Client(api_key=self.serp_api_key)
72
+
73
+ @agent.processor(SERPQuery)
74
+ def search(self, ctx: agent.ProcessContext[SERPQuery]) -> None:
75
+ """
76
+ Handles the received message.
77
+
78
+ Args:
79
+ message (Message): The received message.
80
+ """
81
+ search_query = ctx.payload
82
+
83
+ # Use the message.payload as search parameters
84
+ search_params = SearchEngines[search_query.engine].get_query(search_query.query)
85
+
86
+ # Execute asynchronous search and add it to the queue
87
+ results = self.client.search(
88
+ params=search_params,
89
+ engine=search_query.engine,
90
+ num=search_query.num,
91
+ start=search_query.start,
92
+ )
93
+
94
+ metadata = results["search_metadata"]
95
+
96
+ if metadata["status"] == "Success":
97
+ # Get the search results
98
+ search_results = results["organic_results"]
99
+
100
+ result_links = []
101
+
102
+ # Publish the message
103
+ for result in search_results:
104
+ result.update({"search_parameters": search_params})
105
+
106
+ filepath = urlparse(result["link"]).path
107
+ filename = os.path.basename(filepath)
108
+
109
+ if not filename:
110
+ filename = f"{shortuuid.uuid()}.html"
111
+
112
+ metadata = {
113
+ "title": result["title"],
114
+ "favicon": result.get("favicon", ""),
115
+ "search_position": result["position"],
116
+ "snippet": result["snippet"],
117
+ "date": result.get("date", ""),
118
+ "query_id": search_query.id,
119
+ }
120
+
121
+ link = MediaLink(
122
+ url=result["link"],
123
+ name=filename,
124
+ metadata=metadata,
125
+ mimetype="text/html",
126
+ encoding="utf-8",
127
+ )
128
+
129
+ result_links.append(link)
130
+
131
+ total_results = None
132
+ if "search_information" in results and "total_results" in results["search_information"]:
133
+ total_results = results["search_information"]["total_results"]
134
+
135
+ ctx.send(
136
+ SERPResults(
137
+ count=len(result_links),
138
+ results=result_links,
139
+ total_results=total_results,
140
+ id=search_query.id,
141
+ query=result["search_parameters"]["q"],
142
+ engine=result["search_parameters"]["engine"],
143
+ ),
144
+ new_thread=True,
145
+ )
146
+ else:
147
+ # Publish the error message
148
+ ctx.send(SearchError(response=results["search_metadata"], id=search_query.id))