arugulanet 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Epyphite
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: arugulanet
3
+ Version: 0.1.0
4
+ Summary: Find offers that fit a request through Arugula, with LangChain, LangGraph and CrewAI tools.
5
+ License: MIT
6
+ Project-URL: Homepage, https://arugulanet.com
7
+ Keywords: arugulanet,offers,langchain,langgraph,crewai,agents,tools
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: langchain
15
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
16
+ Provides-Extra: langgraph
17
+ Requires-Dist: langchain-core>=0.3; extra == "langgraph"
18
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
19
+ Provides-Extra: crewai
20
+ Requires-Dist: crewai>=0.80; extra == "crewai"
21
+ Dynamic: license-file
22
+
23
+ # arugulanet
24
+
25
+ Find offers that fit a request through Arugula, for Python agents. It uses an anonymous session, so
26
+ no account is needed. Every offer comes with a tracked link the user follows to buy. The core has no
27
+ dependencies.
28
+
29
+ Arugula is a matching engine: offers are ranked by fit only. The tools return a `disclosure` with the
30
+ offers; show it to the user with them: "Ranked by fit only. Arugula may earn a commission if you buy through these links, at no extra cost to you."
31
+
32
+ ```python
33
+ from arugulanet import Arugula
34
+
35
+ offers = Arugula().find_offers("a hotel room in Singapore for two nights", party_size=2)
36
+ ```
37
+
38
+ ## LangChain and LangGraph
39
+
40
+ Install with `pip install "arugulanet[langchain]"`, or `"arugulanet[langgraph]"`. The tool sends channel
41
+ `langchain`.
42
+
43
+ ```python
44
+ from arugulanet.langchain import find_offers_tool
45
+
46
+ tools = [find_offers_tool()] # also for LangGraph: create_react_agent(model, tools)
47
+ ```
48
+
49
+ ## CrewAI
50
+
51
+ Install with `pip install "arugulanet[crewai]"`. The tool sends channel `crewai`.
52
+
53
+ ```python
54
+ from arugulanet.crewai import FindOffersTool
55
+
56
+ agent = Agent(role="Shopper", goal="Find what the user needs", backstory="...", tools=[FindOffersTool()])
57
+ ```
58
+
59
+ ## More
60
+
61
+ - **Example:** `python examples/agent.py "your request"`. Set `ARUGULA_API_URL` to use another API.
62
+ - **Build:** `python -m build`.
63
+ - **Publish:** `twine upload dist/*`, with the PyPI organization account.
@@ -0,0 +1,41 @@
1
+ # arugulanet
2
+
3
+ Find offers that fit a request through Arugula, for Python agents. It uses an anonymous session, so
4
+ no account is needed. Every offer comes with a tracked link the user follows to buy. The core has no
5
+ dependencies.
6
+
7
+ Arugula is a matching engine: offers are ranked by fit only. The tools return a `disclosure` with the
8
+ offers; show it to the user with them: "Ranked by fit only. Arugula may earn a commission if you buy through these links, at no extra cost to you."
9
+
10
+ ```python
11
+ from arugulanet import Arugula
12
+
13
+ offers = Arugula().find_offers("a hotel room in Singapore for two nights", party_size=2)
14
+ ```
15
+
16
+ ## LangChain and LangGraph
17
+
18
+ Install with `pip install "arugulanet[langchain]"`, or `"arugulanet[langgraph]"`. The tool sends channel
19
+ `langchain`.
20
+
21
+ ```python
22
+ from arugulanet.langchain import find_offers_tool
23
+
24
+ tools = [find_offers_tool()] # also for LangGraph: create_react_agent(model, tools)
25
+ ```
26
+
27
+ ## CrewAI
28
+
29
+ Install with `pip install "arugulanet[crewai]"`. The tool sends channel `crewai`.
30
+
31
+ ```python
32
+ from arugulanet.crewai import FindOffersTool
33
+
34
+ agent = Agent(role="Shopper", goal="Find what the user needs", backstory="...", tools=[FindOffersTool()])
35
+ ```
36
+
37
+ ## More
38
+
39
+ - **Example:** `python examples/agent.py "your request"`. Set `ARUGULA_API_URL` to use another API.
40
+ - **Build:** `python -m build`.
41
+ - **Publish:** `twine upload dist/*`, with the PyPI organization account.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "arugulanet"
7
+ version = "0.1.0"
8
+ description = "Find offers that fit a request through Arugula, with LangChain, LangGraph and CrewAI tools."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["arugulanet", "offers", "langchain", "langgraph", "crewai", "agents", "tools"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = []
19
+
20
+ [project.optional-dependencies]
21
+ langchain = ["langchain-core>=0.3"]
22
+ langgraph = ["langchain-core>=0.3", "langgraph>=0.2"]
23
+ crewai = ["crewai>=0.80"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://arugulanet.com"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """Arugula for Python agents: find offers that fit a request, with LangChain, LangGraph and CrewAI tools.
2
+
3
+ from arugulanet import Arugula
4
+ offers = Arugula().find_offers("a hotel room in Singapore for two nights", party_size=2)
5
+ """
6
+
7
+ from .client import Arugula, ArugulaError
8
+ from .tools import DISCLOSURE, FIND_OFFERS_DESCRIPTION, FIND_OFFERS_SCHEMA, run_find_offers
9
+
10
+ __all__ = ["Arugula", "ArugulaError", "DISCLOSURE", "FIND_OFFERS_DESCRIPTION", "FIND_OFFERS_SCHEMA", "run_find_offers"]
11
+ __version__ = "0.1.0"
@@ -0,0 +1,91 @@
1
+ """A small client for Arugula's public API, on the standard library only.
2
+
3
+ There is no generated Python client yet, so this calls the four public operations it needs by hand:
4
+ start an anonymous session, mint a request, declare it, and read its ranked offers. Responses are
5
+ returned as the API's JSON (dicts), following the API contract (backend api/openapi.yaml).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ import urllib.error
13
+ import urllib.request
14
+ import uuid
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ DEFAULT_API = "https://api.arugulanet.com"
18
+
19
+
20
+ class ArugulaError(Exception):
21
+ """An API error, with its HTTP status and the server's message."""
22
+
23
+ def __init__(self, status: int, message: str) -> None:
24
+ super().__init__(message)
25
+ self.status = status
26
+
27
+
28
+ class Arugula:
29
+ """One anonymous session on Arugula's public API, started on first use and renewed when refused."""
30
+
31
+ def __init__(self, base_url: str = DEFAULT_API, channel: str = "langchain", timeout: float = 30.0) -> None:
32
+ base = base_url.rstrip("/")
33
+ self.base_url = base[: -len("/v1")] if base.endswith("/v1") else base
34
+ self.channel = channel
35
+ self.timeout = timeout
36
+ self._token: Optional[str] = None
37
+
38
+ def _call(self, method: str, path: str, body: Optional[Dict[str, Any]] = None, auth: bool = True) -> Dict[str, Any]:
39
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
40
+ if auth:
41
+ headers["Authorization"] = "Bearer " + self._session()
42
+ if method == "POST" and path != "/v1/public/sessions":
43
+ headers["Idempotency-Key"] = str(uuid.uuid4())
44
+ data = json.dumps(body).encode() if body is not None else None
45
+ request = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
46
+ try:
47
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
48
+ text = response.read().decode()
49
+ return json.loads(text) if text else {}
50
+ except urllib.error.HTTPError as error:
51
+ text = error.read().decode(errors="replace")
52
+ try:
53
+ problem = json.loads(text)
54
+ message = problem.get("detail") or problem.get("title") or text
55
+ except ValueError:
56
+ message = text or str(error)
57
+ raise ArugulaError(error.code, message) from None
58
+
59
+ def _session(self) -> str:
60
+ if self._token is None:
61
+ session = self._call("POST", "/v1/public/sessions", {"channel": self.channel}, auth=False)
62
+ self._token = session["session_token"]
63
+ return self._token
64
+
65
+ def _authorized(self, method: str, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
66
+ try:
67
+ return self._call(method, path, body)
68
+ except ArugulaError as error:
69
+ if error.status != 401:
70
+ raise
71
+ self._token = None
72
+ return self._call(method, path, body)
73
+
74
+ def find_offers(self, request: str, *, wait: float = 30.0, limit: int = 5, **stated: Any) -> List[Dict[str, Any]]:
75
+ """The offers that fit a request, best first; empty when it is too vague to act on or nothing fits.
76
+
77
+ Keyword arguments are the stated context: location, dates, budget ({"amount", "currency"}),
78
+ party_size, preferences.
79
+ """
80
+ context = {key: value for key, value in stated.items() if value not in (None, "")}
81
+ minted = self._authorized("POST", "/v1/intents:mint", {"text": request, **({"stated": context} if context else {})})
82
+ draft = minted["draft"]
83
+ if draft["entailment"]["status"] != "passed":
84
+ return []
85
+ declared = self._authorized("POST", "/v1/intents", {"draft_id": draft["intent_id"]})
86
+ deadline = time.monotonic() + wait
87
+ while True:
88
+ ranked = self._authorized("GET", "/v1/intents/" + declared["demand_id"] + "/offers")
89
+ if ranked.get("run_id") is not None or ranked.get("data") or time.monotonic() > deadline:
90
+ return sorted(ranked.get("data", []), key=lambda offer: offer.get("rank", 0))[:limit]
91
+ time.sleep(1.5)
@@ -0,0 +1,46 @@
1
+ """CrewAI tool: pip install "arugulanet[crewai]".
2
+
3
+ from arugulanet.crewai import FindOffersTool
4
+ agent = Agent(role="Shopper", goal="...", backstory="...", tools=[FindOffersTool()])
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional, Type
10
+
11
+ from crewai.tools import BaseTool
12
+ from pydantic import BaseModel, Field, PrivateAttr
13
+
14
+ from .client import Arugula
15
+ from .tools import FIND_OFFERS_DESCRIPTION, run_find_offers
16
+
17
+
18
+ class FindOffersInput(BaseModel):
19
+ request: str = Field(description="What the user needs, in their own words.")
20
+ location: Optional[str] = Field(default=None, description="Where, if the user said.")
21
+ dates: Optional[str] = Field(default=None, description="When, if the user said.")
22
+ budget_amount: Optional[float] = Field(default=None, description="The user's budget, if they said.")
23
+ budget_currency: Optional[str] = Field(default=None, description="ISO 4217 currency of the budget.")
24
+ party_size: Optional[int] = Field(default=None, description="How many people, if relevant.")
25
+ preferences: Optional[str] = Field(default=None, description="Anything else the user prefers.")
26
+
27
+
28
+ class FindOffersTool(BaseTool):
29
+ """The find_offers tool on channel crewai."""
30
+
31
+ name: str = "find_offers"
32
+ description: str = FIND_OFFERS_DESCRIPTION
33
+ args_schema: Type[BaseModel] = FindOffersInput
34
+ _client: Arugula = PrivateAttr(default_factory=lambda: Arugula(channel="crewai"))
35
+
36
+ def _run(
37
+ self,
38
+ request: str,
39
+ location: Optional[str] = None,
40
+ dates: Optional[str] = None,
41
+ budget_amount: Optional[float] = None,
42
+ budget_currency: Optional[str] = None,
43
+ party_size: Optional[int] = None,
44
+ preferences: Optional[str] = None,
45
+ ) -> str:
46
+ return run_find_offers(self._client, request, location, dates, budget_amount, budget_currency, party_size, preferences)
@@ -0,0 +1,34 @@
1
+ """LangChain (and LangGraph) tool: pip install "arugulanet[langchain]".
2
+
3
+ from arugulanet.langchain import find_offers_tool
4
+ tools = [find_offers_tool()]
5
+
6
+ LangGraph agents take the same tool, for example create_react_agent(model, [find_offers_tool()]).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional
12
+
13
+ from langchain_core.tools import StructuredTool
14
+
15
+ from .client import Arugula
16
+ from .tools import FIND_OFFERS_DESCRIPTION, run_find_offers
17
+
18
+
19
+ def find_offers_tool(client: Optional[Arugula] = None) -> StructuredTool:
20
+ """The find_offers tool on channel langchain."""
21
+ agent = client or Arugula(channel="langchain")
22
+
23
+ def find_offers(
24
+ request: str,
25
+ location: Optional[str] = None,
26
+ dates: Optional[str] = None,
27
+ budget_amount: Optional[float] = None,
28
+ budget_currency: Optional[str] = None,
29
+ party_size: Optional[int] = None,
30
+ preferences: Optional[str] = None,
31
+ ) -> str:
32
+ return run_find_offers(agent, request, location, dates, budget_amount, budget_currency, party_size, preferences)
33
+
34
+ return StructuredTool.from_function(func=find_offers, name="find_offers", description=FIND_OFFERS_DESCRIPTION)
@@ -0,0 +1,60 @@
1
+ """The find_offers tool, shared by the LangChain, LangGraph and CrewAI wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .client import Arugula
9
+
10
+ FIND_OFFERS_DESCRIPTION = (
11
+ "Find offers from sellers and listings that fit what the user needs, for example a product, a stay or a service. "
12
+ "Returns up to five offers ranked by fit, each with a title, a price when known, why it fits, and a link the user follows to buy, "
13
+ "plus a disclosure to show the user with them."
14
+ )
15
+
16
+ DISCLOSURE = "Ranked by fit only. Arugula may earn a commission if you buy through these links, at no extra cost to you."
17
+
18
+ FIND_OFFERS_SCHEMA: Dict[str, Any] = {
19
+ "type": "object",
20
+ "properties": {
21
+ "request": {"type": "string", "description": "What the user needs, in their own words."},
22
+ "location": {"type": "string", "description": "Where, if the user said."},
23
+ "dates": {"type": "string", "description": "When, if the user said."},
24
+ "budget_amount": {"type": "number", "description": "The user's budget, if they said."},
25
+ "budget_currency": {"type": "string", "description": "ISO 4217 currency of the budget, for example SGD."},
26
+ "party_size": {"type": "integer", "description": "How many people, if relevant."},
27
+ "preferences": {"type": "string", "description": "Anything else the user prefers."},
28
+ },
29
+ "required": ["request"],
30
+ }
31
+
32
+
33
+ def run_find_offers(
34
+ client: Arugula,
35
+ request: str,
36
+ location: Optional[str] = None,
37
+ dates: Optional[str] = None,
38
+ budget_amount: Optional[float] = None,
39
+ budget_currency: Optional[str] = None,
40
+ party_size: Optional[int] = None,
41
+ preferences: Optional[str] = None,
42
+ ) -> str:
43
+ """Runs find_offers and returns the offers as compact JSON for a model."""
44
+ budget = {"amount": str(budget_amount), "currency": (budget_currency or "USD").upper()} if budget_amount is not None else None
45
+ offers = client.find_offers(request, location=location, dates=dates, budget=budget, party_size=party_size, preferences=preferences)
46
+ return json.dumps(
47
+ {
48
+ "disclosure": DISCLOSURE,
49
+ "offers": [
50
+ {
51
+ "title": offer.get("title") or offer.get("capability"),
52
+ "price": offer.get("price"),
53
+ "why": offer.get("rationale"),
54
+ "link": offer.get("tracked_url"),
55
+ "how_to_buy": offer.get("how_to_buy"),
56
+ }
57
+ for offer in offers
58
+ ],
59
+ }
60
+ )
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: arugulanet
3
+ Version: 0.1.0
4
+ Summary: Find offers that fit a request through Arugula, with LangChain, LangGraph and CrewAI tools.
5
+ License: MIT
6
+ Project-URL: Homepage, https://arugulanet.com
7
+ Keywords: arugulanet,offers,langchain,langgraph,crewai,agents,tools
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: langchain
15
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
16
+ Provides-Extra: langgraph
17
+ Requires-Dist: langchain-core>=0.3; extra == "langgraph"
18
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
19
+ Provides-Extra: crewai
20
+ Requires-Dist: crewai>=0.80; extra == "crewai"
21
+ Dynamic: license-file
22
+
23
+ # arugulanet
24
+
25
+ Find offers that fit a request through Arugula, for Python agents. It uses an anonymous session, so
26
+ no account is needed. Every offer comes with a tracked link the user follows to buy. The core has no
27
+ dependencies.
28
+
29
+ Arugula is a matching engine: offers are ranked by fit only. The tools return a `disclosure` with the
30
+ offers; show it to the user with them: "Ranked by fit only. Arugula may earn a commission if you buy through these links, at no extra cost to you."
31
+
32
+ ```python
33
+ from arugulanet import Arugula
34
+
35
+ offers = Arugula().find_offers("a hotel room in Singapore for two nights", party_size=2)
36
+ ```
37
+
38
+ ## LangChain and LangGraph
39
+
40
+ Install with `pip install "arugulanet[langchain]"`, or `"arugulanet[langgraph]"`. The tool sends channel
41
+ `langchain`.
42
+
43
+ ```python
44
+ from arugulanet.langchain import find_offers_tool
45
+
46
+ tools = [find_offers_tool()] # also for LangGraph: create_react_agent(model, tools)
47
+ ```
48
+
49
+ ## CrewAI
50
+
51
+ Install with `pip install "arugulanet[crewai]"`. The tool sends channel `crewai`.
52
+
53
+ ```python
54
+ from arugulanet.crewai import FindOffersTool
55
+
56
+ agent = Agent(role="Shopper", goal="Find what the user needs", backstory="...", tools=[FindOffersTool()])
57
+ ```
58
+
59
+ ## More
60
+
61
+ - **Example:** `python examples/agent.py "your request"`. Set `ARUGULA_API_URL` to use another API.
62
+ - **Build:** `python -m build`.
63
+ - **Publish:** `twine upload dist/*`, with the PyPI organization account.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/arugulanet/__init__.py
5
+ src/arugulanet/client.py
6
+ src/arugulanet/crewai.py
7
+ src/arugulanet/langchain.py
8
+ src/arugulanet/tools.py
9
+ src/arugulanet.egg-info/PKG-INFO
10
+ src/arugulanet.egg-info/SOURCES.txt
11
+ src/arugulanet.egg-info/dependency_links.txt
12
+ src/arugulanet.egg-info/requires.txt
13
+ src/arugulanet.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+
2
+ [crewai]
3
+ crewai>=0.80
4
+
5
+ [langchain]
6
+ langchain-core>=0.3
7
+
8
+ [langgraph]
9
+ langchain-core>=0.3
10
+ langgraph>=0.2
@@ -0,0 +1 @@
1
+ arugulanet