agentstr 0.1.10__py3-none-any.whl → 0.1.12__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- agentstr/__init__.py +20 -13
- agentstr/buyer.py +291 -0
- agentstr/buyer.pyi +31 -0
- agentstr/{marketplace.py → merchant.py} +126 -323
- agentstr/merchant.pyi +37 -0
- agentstr/models.py +381 -0
- agentstr/models.pyi +103 -0
- agentstr/nostr.py +389 -53
- agentstr/nostr.pyi +82 -0
- agentstr/py.typed +0 -0
- agentstr-0.1.12.dist-info/METADATA +129 -0
- agentstr-0.1.12.dist-info/RECORD +15 -0
- agentstr/examples/basic_cli/.env.example +0 -2
- agentstr/examples/basic_cli/README.md +0 -11
- agentstr/examples/basic_cli/main.py +0 -193
- agentstr-0.1.10.dist-info/METADATA +0 -133
- agentstr-0.1.10.dist-info/RECORD +0 -11
- {agentstr-0.1.10.dist-info → agentstr-0.1.12.dist-info}/LICENSE +0 -0
- {agentstr-0.1.10.dist-info → agentstr-0.1.12.dist-info}/WHEEL +0 -0
- {agentstr-0.1.10.dist-info → agentstr-0.1.12.dist-info}/top_level.txt +0 -0
agentstr/__init__.py
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
"""
|
2
|
-
AgentStr: Nostr extension for
|
2
|
+
AgentStr: Nostr extension for Agno AI agents
|
3
3
|
"""
|
4
4
|
|
5
|
+
from nostr_sdk import ShippingCost, ShippingMethod # type: ignore
|
6
|
+
|
7
|
+
from .merchant import MerchantTools
|
8
|
+
|
5
9
|
# Import main classes to make them available at package level
|
6
|
-
from .
|
7
|
-
Merchant,
|
8
|
-
MerchantProduct,
|
9
|
-
MerchantStall,
|
10
|
-
Profile,
|
11
|
-
ShippingCost,
|
12
|
-
ShippingMethod,
|
13
|
-
)
|
10
|
+
from .models import AgentProfile, MerchantProduct, MerchantStall, NostrProfile
|
14
11
|
|
15
12
|
# Import version from pyproject.toml at runtime
|
16
13
|
try:
|
@@ -21,10 +18,20 @@ except Exception:
|
|
21
18
|
__version__ = "unknown"
|
22
19
|
|
23
20
|
__all__ = [
|
24
|
-
"
|
25
|
-
"Merchant",
|
26
|
-
"MerchantStall",
|
21
|
+
"MerchantTools",
|
27
22
|
"MerchantProduct",
|
28
|
-
"
|
23
|
+
"MerchantStall",
|
29
24
|
"ShippingCost",
|
25
|
+
"ShippingMethod",
|
26
|
+
]
|
27
|
+
|
28
|
+
from agentstr.nostr import EventId, Keys, NostrClient, ProductData, StallData
|
29
|
+
|
30
|
+
__all__ = [
|
31
|
+
"EventId",
|
32
|
+
"Keys",
|
33
|
+
"NostrClient",
|
34
|
+
"ProductData",
|
35
|
+
"StallData",
|
36
|
+
"AgentProfile",
|
30
37
|
]
|
agentstr/buyer.py
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from uuid import uuid4
|
4
|
+
|
5
|
+
from agno.agent import AgentKnowledge # type: ignore
|
6
|
+
from agno.document.base import Document
|
7
|
+
|
8
|
+
from agentstr.models import AgentProfile, NostrProfile
|
9
|
+
from agentstr.nostr import NostrClient, PublicKey
|
10
|
+
|
11
|
+
try:
|
12
|
+
from agno.tools import Toolkit
|
13
|
+
except ImportError:
|
14
|
+
raise ImportError("`agno` not installed. Please install using `pip install agno`")
|
15
|
+
|
16
|
+
|
17
|
+
def _map_location_to_geohash(location: str) -> str:
|
18
|
+
"""
|
19
|
+
Map a location to a geohash.
|
20
|
+
|
21
|
+
TBD: Implement this function. Returning a fixed geohash for now.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
location: location to map to a geohash. Can be a zip code, city, state, country, or latitude and longitude.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
str: geohash of the location or empty string if location is not found
|
28
|
+
"""
|
29
|
+
if "snoqualmie" in location.lower():
|
30
|
+
return "C23Q7U36W"
|
31
|
+
else:
|
32
|
+
return ""
|
33
|
+
|
34
|
+
|
35
|
+
class BuyerTools(Toolkit):
|
36
|
+
"""
|
37
|
+
BuyerTools is a toolkit that allows an agent to find sellers and transact with them over Nostr.
|
38
|
+
|
39
|
+
Sellers are downloaded from the Nostr relay and cached.
|
40
|
+
Sellers can be found by name or public key.
|
41
|
+
Sellers cache can be refreshed from the Nostr relay.
|
42
|
+
Sellers can be retrieved as a list of Nostr profiles.
|
43
|
+
|
44
|
+
TBD: populate the sellers locations with info from stalls.
|
45
|
+
"""
|
46
|
+
|
47
|
+
from pydantic import ConfigDict
|
48
|
+
|
49
|
+
model_config = ConfigDict(
|
50
|
+
arbitrary_types_allowed=True, extra="allow", validate_assignment=True
|
51
|
+
)
|
52
|
+
|
53
|
+
logger = logging.getLogger("Buyer")
|
54
|
+
sellers: set[NostrProfile] = set()
|
55
|
+
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
knowledge_base: AgentKnowledge,
|
59
|
+
buyer_profile: AgentProfile,
|
60
|
+
relay: str,
|
61
|
+
) -> None:
|
62
|
+
"""Initialize the Buyer toolkit.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
knowledge_base: knowledge base of the buyer agent
|
66
|
+
buyer_profile: profile of the buyer using this agent
|
67
|
+
relay: Nostr relay to use for communications
|
68
|
+
"""
|
69
|
+
super().__init__(name="Buyer")
|
70
|
+
|
71
|
+
self.relay = relay
|
72
|
+
self.buyer_profile = buyer_profile
|
73
|
+
self.knowledge_base = knowledge_base
|
74
|
+
# Initialize fields
|
75
|
+
self._nostr_client = NostrClient(relay, buyer_profile.get_private_key())
|
76
|
+
|
77
|
+
# Register methods
|
78
|
+
self.register(self.find_seller_by_name)
|
79
|
+
self.register(self.find_seller_by_public_key)
|
80
|
+
self.register(self.find_sellers_by_location)
|
81
|
+
self.register(self.get_profile)
|
82
|
+
self.register(self.get_relay)
|
83
|
+
self.register(self.get_seller_stalls)
|
84
|
+
self.register(self.get_seller_products)
|
85
|
+
self.register(self.get_seller_count)
|
86
|
+
self.register(self.get_sellers)
|
87
|
+
self.register(self.refresh_sellers)
|
88
|
+
self.register(self.purchase_product)
|
89
|
+
|
90
|
+
def purchase_product(self, product: str) -> str:
|
91
|
+
"""Purchase a product.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
product: JSON string with product to purchase
|
95
|
+
"""
|
96
|
+
return json.dumps({"status": "success", "message": "Product purchased"})
|
97
|
+
|
98
|
+
def find_seller_by_name(self, name: str) -> str:
|
99
|
+
"""Find a seller by name.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
name: name of the seller to find
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
str: JSON string with seller profile or error message
|
106
|
+
"""
|
107
|
+
for seller in self.sellers:
|
108
|
+
if seller.get_name() == name:
|
109
|
+
response = seller.to_json()
|
110
|
+
# self._store_response_in_knowledge_base(response)
|
111
|
+
return response
|
112
|
+
response = json.dumps({"status": "error", "message": "Seller not found"})
|
113
|
+
self._store_response_in_knowledge_base(response)
|
114
|
+
return response
|
115
|
+
|
116
|
+
def find_seller_by_public_key(self, public_key: str) -> str:
|
117
|
+
"""Find a seller by public key.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
public_key: bech32 encoded public key of the seller to find
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
str: seller profile json string or error message
|
124
|
+
"""
|
125
|
+
for seller in self.sellers:
|
126
|
+
if seller.get_public_key() == public_key:
|
127
|
+
response = seller.to_json()
|
128
|
+
# self._store_response_in_knowledge_base(response)
|
129
|
+
return response
|
130
|
+
response = json.dumps({"status": "error", "message": "Seller not found"})
|
131
|
+
self._store_response_in_knowledge_base(response)
|
132
|
+
return response
|
133
|
+
|
134
|
+
def find_sellers_by_location(self, location: str) -> str:
|
135
|
+
"""Find sellers by location.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
location: location of the seller to find (e.g. "San Francisco, CA")
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
str: list of seller profile json strings or error message
|
142
|
+
"""
|
143
|
+
sellers: set[NostrProfile] = set()
|
144
|
+
geohash = _map_location_to_geohash(location)
|
145
|
+
# print(f"find_sellers_by_location: geohash: {geohash}")
|
146
|
+
|
147
|
+
if not geohash:
|
148
|
+
response = json.dumps({"status": "error", "message": "Invalid location"})
|
149
|
+
return response
|
150
|
+
|
151
|
+
# Find sellers in the same geohash
|
152
|
+
for seller in self.sellers:
|
153
|
+
if geohash in seller.get_locations():
|
154
|
+
# print(
|
155
|
+
# f"geohash {geohash} found in seller {seller.get_name()} with locations {seller.get_locations()}"
|
156
|
+
# )
|
157
|
+
sellers.add(seller)
|
158
|
+
|
159
|
+
if not sellers:
|
160
|
+
response = json.dumps(
|
161
|
+
{"status": "error", "message": f"No sellers found near {location}"}
|
162
|
+
)
|
163
|
+
return response
|
164
|
+
|
165
|
+
response = json.dumps([seller.to_dict() for seller in sellers])
|
166
|
+
# print("find_sellers_by_location: storing response in knowledge base")
|
167
|
+
self._store_response_in_knowledge_base(response)
|
168
|
+
# print(f"Found {len(sellers)} sellers near {location}")
|
169
|
+
return response
|
170
|
+
|
171
|
+
def get_profile(self) -> str:
|
172
|
+
"""Get the Nostr profile of the buyer agent.
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
str: buyer profile json string
|
176
|
+
"""
|
177
|
+
response = self.buyer_profile.to_json()
|
178
|
+
self._store_response_in_knowledge_base(response)
|
179
|
+
return response
|
180
|
+
|
181
|
+
def get_relay(self) -> str:
|
182
|
+
"""Get the Nostr relay that the buyer agent is using.
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
str: Nostr relay
|
186
|
+
"""
|
187
|
+
response = self.relay
|
188
|
+
# self._store_response_in_knowledge_base(response)
|
189
|
+
return response
|
190
|
+
|
191
|
+
def get_seller_stalls(self, public_key: str) -> str:
|
192
|
+
"""Get the stalls from a seller.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
public_key: public key of the seller
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
str: JSON string with seller collections
|
199
|
+
"""
|
200
|
+
try:
|
201
|
+
stalls = self._nostr_client.retrieve_stalls_from_seller(
|
202
|
+
PublicKey.parse(public_key)
|
203
|
+
)
|
204
|
+
response = json.dumps([stall.as_json() for stall in stalls])
|
205
|
+
self._store_response_in_knowledge_base(response)
|
206
|
+
return response
|
207
|
+
except Exception as e:
|
208
|
+
response = json.dumps({"status": "error", "message": str(e)})
|
209
|
+
return response
|
210
|
+
|
211
|
+
def get_seller_count(self) -> str:
|
212
|
+
"""Get the number of sellers.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
str: JSON string with status and count of sellers
|
216
|
+
"""
|
217
|
+
response = json.dumps({"status": "success", "count": len(self.sellers)})
|
218
|
+
return response
|
219
|
+
|
220
|
+
def get_seller_products(self, public_key: str) -> str:
|
221
|
+
"""Get the products from a seller
|
222
|
+
|
223
|
+
Args:
|
224
|
+
public_key: public key of the seller
|
225
|
+
|
226
|
+
Returns:
|
227
|
+
str: JSON string with seller products
|
228
|
+
"""
|
229
|
+
try:
|
230
|
+
products = self._nostr_client.retrieve_products_from_seller(
|
231
|
+
PublicKey.parse(public_key)
|
232
|
+
)
|
233
|
+
|
234
|
+
response = json.dumps([product.to_dict() for product in products])
|
235
|
+
self._store_response_in_knowledge_base(response)
|
236
|
+
return response
|
237
|
+
except Exception as e:
|
238
|
+
response = json.dumps({"status": "error", "message": str(e)})
|
239
|
+
return response
|
240
|
+
|
241
|
+
def get_sellers(self) -> str:
|
242
|
+
"""Get the list of sellers.
|
243
|
+
If no sellers are cached, the list is refreshed from the Nostr relay.
|
244
|
+
If sellers are cached, the list is returned from the cache.
|
245
|
+
To get a fresh list of sellers, call refresh_sellers() sellers first.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
str: list of sellers json strings
|
249
|
+
"""
|
250
|
+
if not self.sellers:
|
251
|
+
self._refresh_sellers()
|
252
|
+
response = json.dumps([seller.to_json() for seller in self.sellers])
|
253
|
+
return response
|
254
|
+
|
255
|
+
def refresh_sellers(self) -> str:
|
256
|
+
"""Refresh the list of sellers.
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
str: JSON string with status and count of sellers refreshed
|
260
|
+
"""
|
261
|
+
self._refresh_sellers()
|
262
|
+
response = json.dumps({"status": "success", "count": len(self.sellers)})
|
263
|
+
return response
|
264
|
+
|
265
|
+
def _refresh_sellers(self) -> None:
|
266
|
+
"""
|
267
|
+
Internal fucntion to retrieve a new list of sellers from the Nostr relay.
|
268
|
+
The old list is discarded and the new list only contains unique sellers currently stored at the relay.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
List[NostrProfile]: List of Nostr profiles of all sellers.
|
272
|
+
"""
|
273
|
+
sellers = self._nostr_client.retrieve_sellers()
|
274
|
+
if len(sellers) == 0:
|
275
|
+
self.logger.info("No sellers found")
|
276
|
+
else:
|
277
|
+
self.logger.info(f"Found {len(sellers)} sellers")
|
278
|
+
|
279
|
+
# Print the locations of the sellers
|
280
|
+
# for seller in sellers:
|
281
|
+
# print(f"Seller {seller.get_name()} has locations {seller.get_locations()}")
|
282
|
+
|
283
|
+
self.sellers = sellers
|
284
|
+
|
285
|
+
def _store_response_in_knowledge_base(self, response: str) -> None:
|
286
|
+
doc = Document(
|
287
|
+
id=str(uuid4()),
|
288
|
+
content=response,
|
289
|
+
)
|
290
|
+
# print(f"Document length: {len(doc.content.split())} words")
|
291
|
+
self.knowledge_base.load_documents([doc]) # Store response in Cassandra
|
agentstr/buyer.pyi
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from logging import Logger
|
2
|
+
from typing import ClassVar
|
3
|
+
|
4
|
+
from agno.agent import AgentKnowledge
|
5
|
+
from agno.tools import Toolkit
|
6
|
+
|
7
|
+
from agentstr.models import AgentProfile, NostrProfile
|
8
|
+
from agentstr.nostr import NostrClient
|
9
|
+
|
10
|
+
class BuyerTools(Toolkit):
|
11
|
+
logger: ClassVar[Logger]
|
12
|
+
sellers: set[NostrProfile]
|
13
|
+
relay: str
|
14
|
+
_nostr_client: NostrClient
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self, knowledge_base: AgentKnowledge, buyer_profile: AgentProfile, relay: str
|
18
|
+
) -> None: ...
|
19
|
+
def find_seller_by_name(self, name: str) -> str: ...
|
20
|
+
def find_seller_by_public_key(self, public_key: str) -> str: ...
|
21
|
+
def find_sellers_by_location(self, location: str) -> str: ...
|
22
|
+
def get_profile(self) -> str: ...
|
23
|
+
def get_relay(self) -> str: ...
|
24
|
+
def get_seller_stalls(self, public_key: str) -> str: ...
|
25
|
+
def get_seller_count(self) -> str: ...
|
26
|
+
def get_seller_products(self, public_key: str) -> str: ...
|
27
|
+
def get_sellers(self) -> str: ...
|
28
|
+
def purchase_product(self, product: str) -> str: ...
|
29
|
+
def refresh_sellers(self) -> str: ...
|
30
|
+
def _refresh_sellers(self) -> None: ...
|
31
|
+
def _store_response_in_knowledge_base(self, response: str) -> None: ...
|