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