actuent 0.2.0__py3-none-any.whl

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.
actuent/__init__.py ADDED
@@ -0,0 +1,207 @@
1
+ """Actuent — the search engine for AI agents.
2
+
3
+ from actuent import Actuent
4
+ client = Actuent() # free tier, or Actuent(api_key="ak_...") for Pro
5
+ results = client.search("barber amsterdam")
6
+ shoes = client.search("running shoes under €100")["products"]
7
+ evening = client.plan("Nørreport, Copenhagen", stops=["dinner", "drinks"])
8
+ fade = client.find_service("skin fade under €30", location="Amsterdam")
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ from typing import Any, Dict, Optional
17
+
18
+ __version__ = "0.2.0"
19
+ __all__ = ["Actuent", "ActuentError", "RateLimitError"]
20
+
21
+ _USER_AGENT = f"actuent-python/{__version__}"
22
+
23
+
24
+ class ActuentError(Exception):
25
+ """An error response from Actuent."""
26
+
27
+ def __init__(self, message: str, status: Optional[int] = None, body: Any = None):
28
+ super().__init__(message)
29
+ self.status = status
30
+ self.body = body
31
+
32
+
33
+ class RateLimitError(ActuentError):
34
+ """Too many requests. `retry_after` is the number of seconds to wait."""
35
+
36
+ def __init__(self, message: str, retry_after: Optional[int] = None, body: Any = None):
37
+ super().__init__(message, 429, body)
38
+ self.retry_after = retry_after
39
+
40
+
41
+ class Actuent:
42
+ """Client for the Actuent API (https://docs.actuent.ai).
43
+
44
+ api_key: optional Pro key from actuent.ai. Defaults to the ACTUENT_API_KEY environment variable.
45
+ Without a key you get the free tier (20 requests/minute).
46
+ """
47
+
48
+ def __init__(self, api_key: Optional[str] = None, base_url: str = "https://api.actuent.ai",
49
+ agents_url: str = "https://agents.actuent.ai", timeout: float = 60.0):
50
+ self.api_key = api_key if api_key is not None else os.environ.get("ACTUENT_API_KEY")
51
+ self.base_url = base_url.rstrip("/")
52
+ self.agents_url = agents_url.rstrip("/")
53
+ self.timeout = timeout
54
+ self.rate_limit: Dict[str, Optional[int]] = {}
55
+
56
+ def _request(self, method: str, url: str, body: Optional[Dict[str, Any]] = None) -> Any:
57
+ headers = {"Accept": "application/json", "User-Agent": _USER_AGENT}
58
+ data = None
59
+ if body is not None:
60
+ data = json.dumps(body).encode("utf-8")
61
+ headers["Content-Type"] = "application/json"
62
+ if self.api_key:
63
+ headers["Authorization"] = f"Bearer {self.api_key}"
64
+ request = urllib.request.Request(url, data=data, headers=headers, method=method)
65
+ try:
66
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
67
+ self._remember_limits(response.headers)
68
+ return json.loads(response.read().decode("utf-8") or "null")
69
+ except urllib.error.HTTPError as error:
70
+ self._remember_limits(error.headers)
71
+ try:
72
+ payload = json.loads(error.read().decode("utf-8") or "null")
73
+ except ValueError:
74
+ payload = None
75
+ message = (payload or {}).get("error") or (payload or {}).get("message") or f"HTTP {error.code}"
76
+ if error.code == 429:
77
+ retry = error.headers.get("Retry-After")
78
+ raise RateLimitError(message, int(retry) if retry and retry.isdigit() else None, payload) from None
79
+ raise ActuentError(message, error.code, payload) from None
80
+
81
+ def _remember_limits(self, headers: Any) -> None:
82
+ def number(name: str) -> Optional[int]:
83
+ value = headers.get(name) if headers else None
84
+ return int(value) if value and str(value).isdigit() else None
85
+ self.rate_limit = {"limit": number("X-RateLimit-Limit"), "remaining": number("X-RateLimit-Remaining"),
86
+ "reset": number("X-RateLimit-Reset")}
87
+
88
+ def search(self, query: str) -> Dict[str, Any]:
89
+ """Search by topic, domain or page, in any language. Returns {"results": [...LAWP], "products": [...]}.
90
+
91
+ Examples: "barber amsterdam", "nike.com", "stripe.com/pricing", "running shoes under €100".
92
+ """
93
+ return self._request("GET", f"{self.base_url}/api/search?q={urllib.parse.quote(query)}")
94
+
95
+ def get_site(self, domain: str) -> Optional[Dict[str, Any]]:
96
+ """The LAWP for one site (or None if Actuent can't find it)."""
97
+ results = self.search(domain).get("results") or []
98
+ return results[0] if results else None
99
+
100
+ def get_page(self, url: str) -> Optional[Dict[str, Any]]:
101
+ """The LAWP for one page, e.g. "stripe.com/pricing"."""
102
+ return self.get_site(url)
103
+
104
+ def products(self, query: str) -> list:
105
+ """Products with prices, e.g. "running shoes under €100"."""
106
+ return self.search(query).get("products") or []
107
+
108
+ def check_site(self, domain: str) -> Dict[str, Any]:
109
+ """Validate a site's /.well-known/lawp.json and its action endpoints."""
110
+ return self._request("GET", f"{self.agents_url}/api/lawp-check?domain={urllib.parse.quote(domain)}")
111
+
112
+ def register(self, site: Dict[str, Any]) -> Dict[str, Any]:
113
+ """List or update your own site's LAWP (Pro key, verified domain). See docs.actuent.ai/#sdk."""
114
+ return self._request("POST", f"{self.base_url}/api/register", site)
115
+
116
+ def state(self) -> Dict[str, Any]:
117
+ """Live "State of the AI web" statistics."""
118
+ return self._request("GET", f"{self.base_url}/api/state")
119
+
120
+ # ----- Tools from the Actuent MCP server (the same ones ChatGPT and Claude use) -----
121
+
122
+ def _tool(self, name: str, arguments: Dict[str, Any]) -> Any:
123
+ args = {k: v for k, v in arguments.items() if v is not None}
124
+ reply = self._request("POST", f"{self.agents_url}/api/mcp",
125
+ {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": name, "arguments": args}})
126
+ if not isinstance(reply, dict) or "result" not in reply:
127
+ raise ActuentError((reply or {}).get("error", {}).get("message", "Unexpected response"), None, reply)
128
+ result = reply["result"]
129
+ text = "".join(part.get("text", "") for part in result.get("content", []) if part.get("type") == "text")
130
+ try:
131
+ data = json.loads(text)
132
+ except ValueError:
133
+ data = text
134
+ if result.get("isError"):
135
+ message = data.get("error") if isinstance(data, dict) else str(data)
136
+ raise ActuentError(message or f"{name} failed", None, data)
137
+ return data
138
+
139
+ def get_actions(self, domain: str) -> Dict[str, Any]:
140
+ """A site's actions, whether each is executable, and the JSON Schema of each input."""
141
+ return self._tool("actuent_get_actions", {"domain": domain})
142
+
143
+ def ask_site(self, domain: str, question: str) -> Dict[str, Any]:
144
+ """Answer a question from one site's own pages, e.g. ("nike.com", "free returns?")."""
145
+ return self._tool("actuent_ask_site", {"domain": domain, "question": question})
146
+
147
+ def nearby(self, query: str, location: str, radius_metres: Optional[int] = None, open_now: Optional[bool] = None,
148
+ filters: Optional[list] = None) -> Dict[str, Any]:
149
+ """Places near a location. filters: vegan, vegetarian, gluten_free, wheelchair, outdoor_seating, wifi, kids, dogs."""
150
+ return self._tool("actuent_nearby", {"query": query, "location": location, "radius_metres": radius_metres,
151
+ "open_now": open_now, "filters": filters})
152
+
153
+ def find_service(self, query: str, location: Optional[str] = None, max_price: Optional[float] = None,
154
+ currency: Optional[str] = None) -> Dict[str, Any]:
155
+ """A service or dish with its price at local businesses, e.g. "skin fade under €30"."""
156
+ return self._tool("actuent_find_service", {"query": query, "location": location, "max_price": max_price, "currency": currency})
157
+
158
+ def plan(self, location: str, stops: Optional[list] = None, date: Optional[str] = None, start_time: Optional[str] = None,
159
+ cuisine: Optional[str] = None, filters: Optional[list] = None) -> Dict[str, Any]:
160
+ """A timed outing, e.g. stops=["dinner", "drinks"] from 19:00, with places open when you'd arrive."""
161
+ return self._tool("actuent_plan", {"location": location, "stops": stops, "date": date, "start_time": start_time,
162
+ "cuisine": cuisine, "filters": filters})
163
+
164
+ def trip(self, location: str, days: int = 2, start_date: Optional[str] = None, filters: Optional[list] = None) -> Dict[str, Any]:
165
+ """A 1–4 day city trip: where to stay and a plan for each day."""
166
+ return self._tool("actuent_trip", {"location": location, "days": days, "start_date": start_date, "filters": filters})
167
+
168
+ def events(self, location: Optional[str] = None, query: Optional[str] = None, date_from: Optional[str] = None,
169
+ date_to: Optional[str] = None) -> Dict[str, Any]:
170
+ """Upcoming events that websites publish, by city, topic and dates (YYYY-MM-DD)."""
171
+ return self._tool("actuent_events", {"location": location, "query": query, "from": date_from, "to": date_to})
172
+
173
+ def compare_sites(self, domains: list) -> Any:
174
+ """Compare two or more sites side by side."""
175
+ return self._tool("actuent_compare", {"domains": domains})
176
+
177
+ def compare_products(self, urls: list) -> Dict[str, Any]:
178
+ """Compare products by URL: price, stock, 90-day price range and cheaper shops."""
179
+ return self._tool("actuent_compare", {"products": urls})
180
+
181
+ def news(self, topic: str) -> Any:
182
+ """Latest articles on a topic."""
183
+ return self._tool("actuent_news", {"topic": topic})
184
+
185
+ def watch_price(self, url: str, target_price_eur: Optional[float] = None, notify: str = "price",
186
+ webhook_url: Optional[str] = None) -> Dict[str, Any]:
187
+ """(Pro) Get an email, and optionally a webhook, when a product's price drops or it's back in stock
188
+ (notify="price", "stock" or "both")."""
189
+ return self._tool("actuent_watch_price", {"url": url, "target_price_eur": target_price_eur, "notify": notify, "webhook_url": webhook_url})
190
+
191
+ def price_watches(self) -> Dict[str, Any]:
192
+ """(Pro) Your price watches."""
193
+ return self._tool("actuent_watch_price", {"action": "list"})
194
+
195
+ def execute_action(self, domain: str, action_id: str, input: Any = None) -> Dict[str, Any]:
196
+ """(Pro) Perform a site's action. Confirm with your user first."""
197
+ return self._tool("actuent_execute_action", {"domain": domain, "action_id": action_id, "input": input})
198
+
199
+ # ----- Scores and badges -----
200
+
201
+ def score(self, domain: str) -> Dict[str, Any]:
202
+ """A site's agent-readiness score (0–100), label, category and checks."""
203
+ return self._request("GET", f"{self.base_url}/badge.json?domain={urllib.parse.quote(domain)}")
204
+
205
+ def leaderboard(self) -> Dict[str, Any]:
206
+ """The 100 sites agents found most often this week."""
207
+ return self._request("GET", f"{self.base_url}/leaderboard")
actuent/langchain.py ADDED
@@ -0,0 +1,44 @@
1
+ """LangChain tools for Actuent.
2
+
3
+ from actuent.langchain import get_tools
4
+ tools = get_tools() # or get_tools(api_key="ak_...")
5
+ agent = create_react_agent(llm, tools)
6
+
7
+ Requires: pip install "actuent[langchain]"
8
+ """
9
+
10
+ import json
11
+ from typing import List, Optional, Type
12
+
13
+ from langchain_core.tools import BaseTool
14
+ from pydantic import BaseModel, Field
15
+
16
+ from . import Actuent
17
+
18
+
19
+ class _SearchInput(BaseModel):
20
+ query: str = Field(description="What to find: a topic ('barber amsterdam'), a domain ('nike.com'), "
21
+ "a page ('stripe.com/pricing') or a product search ('running shoes under €100').")
22
+
23
+
24
+ class ActuentSearchTool(BaseTool):
25
+ """Search the web as structured data with Actuent."""
26
+
27
+ name: str = "actuent_search"
28
+ description: str = (
29
+ "Search websites and products for the user. Returns each site's pages summarised in plain English, "
30
+ "the actions a visitor can take (book, contact, buy), and for shopping searches, products with prices. "
31
+ "Use it instead of browsing raw web pages. Works in any language; results are in English."
32
+ )
33
+ args_schema: Type[BaseModel] = _SearchInput
34
+ client: Actuent = Field(default_factory=Actuent, exclude=True)
35
+
36
+ model_config = {"arbitrary_types_allowed": True}
37
+
38
+ def _run(self, query: str, run_manager=None) -> str:
39
+ return json.dumps(self.client.search(query), ensure_ascii=False)
40
+
41
+
42
+ def get_tools(api_key: Optional[str] = None) -> List[BaseTool]:
43
+ """Actuent tools for a LangChain agent."""
44
+ return [ActuentSearchTool(client=Actuent(api_key=api_key))]
actuent/llama_index.py ADDED
@@ -0,0 +1,27 @@
1
+ """LlamaIndex tools for Actuent.
2
+
3
+ from actuent.llama_index import get_tools
4
+ agent = ReActAgent.from_tools(get_tools(), llm=llm)
5
+
6
+ Requires: pip install "actuent[llamaindex]"
7
+ """
8
+
9
+ import json
10
+ from typing import List, Optional
11
+
12
+ from llama_index.core.tools import FunctionTool
13
+
14
+ from . import Actuent
15
+
16
+
17
+ def get_tools(api_key: Optional[str] = None) -> List[FunctionTool]:
18
+ """Actuent tools for a LlamaIndex agent."""
19
+ client = Actuent(api_key=api_key)
20
+
21
+ def actuent_search(query: str) -> str:
22
+ """Search websites and products. Returns each site's pages in plain English, the actions a visitor
23
+ can take (book, contact, buy), and for shopping searches, products with prices. Accepts a topic,
24
+ domain, page (domain/path) or product search like 'running shoes under €100'."""
25
+ return json.dumps(client.search(query), ensure_ascii=False)
26
+
27
+ return [FunctionTool.from_defaults(fn=actuent_search)]
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: actuent
3
+ Version: 0.2.0
4
+ Summary: Python SDK for Actuent — the search engine for AI agents. Websites and products as structured LAWP, plus LangChain and LlamaIndex tools.
5
+ Author-email: localilabs <support@localilabs.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://actuent.ai
8
+ Project-URL: Documentation, https://docs.actuent.ai
9
+ Project-URL: Source, https://github.com/localilabs/actuent-python
10
+ Keywords: ai,agents,search,lawp,mcp,langchain,llamaindex
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: langchain
18
+ Requires-Dist: langchain-core>=0.2; extra == "langchain"
19
+ Provides-Extra: llamaindex
20
+ Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
21
+ Dynamic: license-file
22
+
23
+ # Actuent for Python
24
+
25
+ Search the web as structured data. [Actuent](https://actuent.ai) is a search engine for AI agents: it returns websites as **LAWP** (clean JSON with each site's pages and the actions a visitor can take) and products with prices, instead of raw HTML.
26
+
27
+ ```bash
28
+ pip install actuent
29
+ ```
30
+
31
+ ```python
32
+ from actuent import Actuent
33
+
34
+ client = Actuent() # free tier; Actuent(api_key="ak_...") for Pro
35
+ site = client.get_site("stripe.com") # pages + actions for one site
36
+ results = client.search("barber amsterdam")["results"]
37
+ shoes = client.products("running shoes under €100") # products with prices, in English
38
+ ```
39
+
40
+ Searches work in any language and always come back in English. Free: 20 requests/minute. Pro keys from [actuent.ai](https://actuent.ai): full results and 60/minute. Rate-limit info is in `client.rate_limit`; a `RateLimitError` has `retry_after`.
41
+
42
+ ## LangChain
43
+
44
+ ```bash
45
+ pip install "actuent[langchain]"
46
+ ```
47
+
48
+ ```python
49
+ from actuent.langchain import get_tools
50
+ tools = get_tools() # [ActuentSearchTool]
51
+ ```
52
+
53
+
54
+ ## Plans, services and more (0.2)
55
+
56
+ ```python
57
+ client.plan("Nørreport, Copenhagen", stops=["dinner", "drinks"], start_time="19:00")
58
+ client.trip("Lisbon", days=3)
59
+ client.find_service("skin fade under €30", location="Amsterdam")
60
+ client.nearby("cafe", "Jordaan, Amsterdam", filters=["vegan", "wifi"], open_now=True)
61
+ client.events(location="Copenhagen", query="jazz")
62
+ client.ask_site("nike.com", "free returns?")
63
+ client.compare_products([url_a, url_b])
64
+ client.score("yoursite.com") # agent-readiness 0–100
65
+ client.watch_price(url, notify="both") # Pro: price drops and back in stock
66
+ ```
67
+
68
+ ## LlamaIndex
69
+
70
+ ```bash
71
+ pip install "actuent[llamaindex]"
72
+ ```
73
+
74
+ ```python
75
+ from actuent.llama_index import get_tools
76
+ tools = get_tools() # [FunctionTool actuent_search]
77
+ ```
78
+
79
+ ## More
80
+
81
+ - `client.check_site("yoursite.com")`: validate your `/.well-known/lawp.json` and action endpoints
82
+ - `client.register({...})`: list your own site (Pro, verified domain)
83
+ - `client.state()`: live "State of the AI web" numbers
84
+
85
+ Docs: [docs.actuent.ai](https://docs.actuent.ai) · MCP server for Claude and ChatGPT: `https://agents.actuent.ai/api/mcp`
86
+
87
+ Made by [localilabs](https://localilabs.com). MIT licensed.
@@ -0,0 +1,8 @@
1
+ actuent/__init__.py,sha256=Cy7WFUyfmquhytq8Sq1OBUHaavUvDomSfHbIKk_rkRE,10979
2
+ actuent/langchain.py,sha256=YaBuGH4nY_4_dZDYpBMTpnRskCV5yUC8pO8wFioGcao,1611
3
+ actuent/llama_index.py,sha256=nAYN1EQzjPrYfMvN_YQgHc9Wit6jWQ7CODFV9CCRmKA,935
4
+ actuent-0.2.0.dist-info/licenses/LICENSE,sha256=L4f2H9noHi5v96HErbbs_vfIyTCFL3-Bh5j1_ffVE0g,1067
5
+ actuent-0.2.0.dist-info/METADATA,sha256=LrjubqdBioKqZw7y-Y_clOcIkL0CNrmdwu4IRYj83Kg,3202
6
+ actuent-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ actuent-0.2.0.dist-info/top_level.txt,sha256=ATBdStNCgEYObRjMxY6lZKYD01Hbp2W7K5wDU74-1po,8
8
+ actuent-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 localilabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ actuent