agentstr 0.1.11__py3-none-any.whl → 0.1.12__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,16 +1,13 @@
1
1
  """
2
- AgentStr: Nostr extension for Phidata AI agents
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 .marketplace import (
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
- "Profile",
25
- "Merchant",
26
- "MerchantStall",
21
+ "MerchantTools",
27
22
  "MerchantProduct",
28
- "ShippingMethod",
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: ...