agentstr 0.1.11__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 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: ...