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.
- arugulanet-0.1.0/LICENSE +21 -0
- arugulanet-0.1.0/PKG-INFO +63 -0
- arugulanet-0.1.0/README.md +41 -0
- arugulanet-0.1.0/pyproject.toml +29 -0
- arugulanet-0.1.0/setup.cfg +4 -0
- arugulanet-0.1.0/src/arugulanet/__init__.py +11 -0
- arugulanet-0.1.0/src/arugulanet/client.py +91 -0
- arugulanet-0.1.0/src/arugulanet/crewai.py +46 -0
- arugulanet-0.1.0/src/arugulanet/langchain.py +34 -0
- arugulanet-0.1.0/src/arugulanet/tools.py +60 -0
- arugulanet-0.1.0/src/arugulanet.egg-info/PKG-INFO +63 -0
- arugulanet-0.1.0/src/arugulanet.egg-info/SOURCES.txt +13 -0
- arugulanet-0.1.0/src/arugulanet.egg-info/dependency_links.txt +1 -0
- arugulanet-0.1.0/src/arugulanet.egg-info/requires.txt +10 -0
- arugulanet-0.1.0/src/arugulanet.egg-info/top_level.txt +1 -0
arugulanet-0.1.0/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
arugulanet
|