agentstr 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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: ...