solana-agent 1.4.4__py3-none-any.whl → 2.0.1__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.
solana_agent/ai.py CHANGED
@@ -1,16 +1,12 @@
1
1
  import asyncio
2
2
  import datetime
3
3
  import json
4
- from typing import AsyncGenerator, Literal, Optional, Dict, Any, Callable
4
+ from typing import AsyncGenerator, List, Literal, Dict, Any, Callable
5
5
  import uuid
6
- import cohere
7
6
  import pandas as pd
8
7
  from pydantic import BaseModel
9
8
  from pymongo import MongoClient
10
9
  from openai import OpenAI
11
- from openai import AssistantEventHandler
12
- from openai.types.beta.threads import TextDelta, Text
13
- from typing_extensions import override
14
10
  import inspect
15
11
  import requests
16
12
  from zep_cloud.client import AsyncZep
@@ -19,88 +15,46 @@ from zep_cloud.types import Message
19
15
  from pinecone import Pinecone
20
16
 
21
17
 
22
- class EventHandler(AssistantEventHandler):
23
- def __init__(self, tool_handlers, ai_instance):
24
- super().__init__()
25
- self._tool_handlers = tool_handlers
26
- self._ai_instance = ai_instance
27
-
28
- @override
29
- def on_text_delta(self, delta: TextDelta, snapshot: Text):
30
- asyncio.create_task(
31
- self._ai_instance._accumulated_value_queue.put(delta.value))
32
-
33
- @override
34
- def on_event(self, event):
35
- if event.event == "thread.run.requires_action":
36
- run_id = event.data.id
37
- self._ai_instance._handle_requires_action(event.data, run_id)
38
-
39
-
40
- class ToolConfig(BaseModel):
41
- name: str
42
- description: str
43
- parameters: Dict[str, Any]
18
+ class DocumentModel(BaseModel):
19
+ id: str
20
+ text: str
44
21
 
45
22
 
46
23
  class MongoDatabase:
47
24
  def __init__(self, db_url: str, db_name: str):
48
25
  self._client = MongoClient(db_url)
49
26
  self.db = self._client[db_name]
50
- self._threads = self.db["threads"]
51
27
  self.messages = self.db["messages"]
52
28
  self.kb = self.db["kb"]
53
- self.vector_stores = self.db["vector_stores"]
54
- self.files = self.db["files"]
55
-
56
- def save_thread_id(self, user_id: str, thread_id: str):
57
- self._threads.insert_one({"thread_id": thread_id, "user_id": user_id})
58
-
59
- def get_thread_id(self, user_id: str) -> Optional[str]:
60
- document = self._threads.find_one({"user_id": user_id})
61
- return document["thread_id"] if document else None
62
29
 
63
30
  def save_message(self, user_id: str, metadata: Dict[str, Any]):
64
31
  metadata["user_id"] = user_id
65
32
  self.messages.insert_one(metadata)
66
33
 
67
- def delete_all_threads(self):
68
- self._threads.delete_many({})
69
-
70
34
  def clear_user_history(self, user_id: str):
71
35
  self.messages.delete_many({"user_id": user_id})
72
- self._threads.delete_one({"user_id": user_id})
73
-
74
- def add_document_to_kb(self, id: str, namespace: str, document: str):
75
- storage = {}
76
- storage["namespace"] = namespace
77
- storage["reference"] = id
78
- storage["document"] = document
79
- storage["timestamp"] = datetime.datetime.now(datetime.timezone.utc)
80
- self.kb.insert_one(storage)
81
-
82
- def get_vector_store_id(self) -> str | None:
83
- document = self.vector_stores.find_one()
84
- return document["vector_store_id"] if document else None
85
36
 
86
- def save_vector_store_id(self, vector_store_id: str):
87
- self.vector_stores.insert_one({"vector_store_id": vector_store_id})
37
+ def add_documents_to_kb(self, namespace: str, documents: List[DocumentModel]):
38
+ for document in documents:
39
+ storage = {}
40
+ storage["namespace"] = namespace
41
+ storage["reference"] = document.id
42
+ storage["document"] = document.text
43
+ storage["timestamp"] = datetime.datetime.now(datetime.timezone.utc)
44
+ self.kb.insert_one(storage)
88
45
 
89
- def delete_vector_store_id(self, vector_store_id: str):
90
- self.vector_stores.delete_one({"vector_store_id": vector_store_id})
91
-
92
- def add_file(self, file_id: str):
93
- self.files.insert_one({"file_id": file_id})
94
-
95
- def delete_file(self, file_id: str):
96
- self.files.delete_one({"file_id": file_id})
46
+ def list_documents_in_kb(self, namespace: str) -> List[DocumentModel]:
47
+ documents = self.kb.find({"namespace": namespace})
48
+ return [
49
+ DocumentModel(id=doc["reference"], text=doc["document"])
50
+ for doc in documents
51
+ ]
97
52
 
98
53
 
99
54
  class AI:
100
55
  def __init__(
101
56
  self,
102
57
  openai_api_key: str,
103
- name: str,
104
58
  instructions: str,
105
59
  database: Any,
106
60
  zep_api_key: str = None,
@@ -108,74 +62,55 @@ class AI:
108
62
  grok_api_key: str = None,
109
63
  pinecone_api_key: str = None,
110
64
  pinecone_index_name: str = None,
111
- cohere_api_key: str = None,
112
- cohere_model: Literal["rerank-v3.5"] = "rerank-v3.5",
65
+ pinecone_embed_model: Literal["llama-text-embed-v2"] = "llama-text-embed-v2",
113
66
  gemini_api_key: str = None,
114
- code_interpreter: bool = False,
115
- file_search: bool = False,
116
- openai_assistant_model: Literal["gpt-4o-mini",
117
- "gpt-4o"] = "gpt-4o-mini",
118
- openai_embedding_model: Literal[
119
- "text-embedding-3-small", "text-embedding-3-large"
120
- ] = "text-embedding-3-large",
67
+ openai_base_url: str = None,
68
+ openai_model: str = "gpt-4o-mini",
121
69
  ):
122
- """Initialize a new AI assistant with memory and tool integration capabilities.
70
+ """Initialize a new AI assistant instance.
123
71
 
124
72
  Args:
125
73
  openai_api_key (str): OpenAI API key for core AI functionality
126
- name (str): Name identifier for the assistant
127
74
  instructions (str): Base behavioral instructions for the AI
128
75
  database (Any): Database instance for message/thread storage
129
- zep_api_key (str, optional): API key for Zep memory integration. Defaults to None
76
+ zep_api_key (str, optional): API key for Zep memory storage. Defaults to None
130
77
  perplexity_api_key (str, optional): API key for Perplexity search. Defaults to None
131
78
  grok_api_key (str, optional): API key for X/Twitter search via Grok. Defaults to None
132
79
  pinecone_api_key (str, optional): API key for Pinecone. Defaults to None
133
80
  pinecone_index_name (str, optional): Name of the Pinecone index. Defaults to None
134
- cohere_api_key (str, optional): API key for Cohere search. Defaults to None
135
- cohere_model (Literal["rerank-v3.5"], optional): Cohere model for reranking. Defaults to "rerank-v3.5"
81
+ pinecone_embed_model (Literal["llama-text-embed-v2"], optional): Pinecone embedding model. Defaults to "llama-text-embed-v2"
136
82
  gemini_api_key (str, optional): API key for Gemini search. Defaults to None
137
- code_interpreter (bool, optional): Enable code interpretation. Defaults to False
138
- file_search (bool, optional): Enable file search tool. Defaults to False
139
- openai_assistant_model (Literal["gpt-4o-mini", "gpt-4o"], optional): OpenAI model for assistant. Defaults to "gpt-4o-mini"
140
- openai_embedding_model (Literal["text-embedding-3-small", "text-embedding-3-large"], optional): OpenAI model for text embedding. Defaults to "text-embedding-3-large"
83
+ openai_base_url (str, optional): Base URL for OpenAI API. Defaults to None
84
+ openai_model (str, optional): OpenAI model to use. Defaults to "gpt-4o-mini"
141
85
 
142
86
  Example:
143
87
  ```python
144
88
  ai = AI(
145
89
  openai_api_key="your-key",
146
- name="Assistant",
147
90
  instructions="Be helpful and concise",
148
- database=MongoDatabase("mongodb://localhost", "ai_db"),
91
+ database=MongoDatabase("mongodb://localhost", "ai_db")
149
92
  )
150
93
  ```
151
94
  Notes:
152
95
  - Requires valid OpenAI API key for core functionality
96
+ - Supports any OpenAI compatible model for conversation
97
+ - Requires valid Zep API key for memory storage
153
98
  - Database instance for storing messages and threads
154
- - Optional integrations for Zep, Perplexity, Pinecone, Cohere, Gemini, and Grok
155
- - Supports code interpretation and custom tool functions
99
+ - Optional integrations for Perplexity, Pinecone, Gemini, and Grok
156
100
  - You must create the Pinecone index in the dashboard before using it
157
101
  """
158
- self._client = OpenAI(api_key=openai_api_key)
159
- self._name = name
160
- self._instructions = instructions
161
- self._openai_assistant_model = openai_assistant_model
162
- self._openai_embedding_model = openai_embedding_model
163
- self._file_search = file_search
164
- if file_search:
165
- self._tools = (
166
- [
167
- {"type": "code_interpreter"},
168
- {"type": "file_search"},
169
- ]
170
- if code_interpreter
171
- else [{"type": "file_search"}]
172
- )
173
- else:
174
- self._tools = [{"type": "code_interpreter"}
175
- ] if code_interpreter else []
176
-
177
- self._tool_handlers = {}
178
- self._assistant_id = None
102
+ self._client = OpenAI(api_key=openai_api_key, base_url=openai_base_url) if openai_base_url else OpenAI(
103
+ api_key=openai_api_key)
104
+ memory_instructions = """
105
+ You are a highly intelligent, context-aware conversational AI. When a user sends a query or statement, you should not only process the current input but also retrieve and integrate relevant context from their previous interactions. Use the memory data to:
106
+ - Infer nuances in the user's intent.
107
+ - Recall previous topics, preferences, or facts that might be relevant.
108
+ - Provide a thoughtful, clear, and structured response.
109
+ - Clarify ambiguous queries by relating them to known user history.
110
+
111
+ Always be concise and ensure that your response maintains coherence across the conversation while respecting the user's context and previous data.
112
+ """
113
+ self._instructions = instructions + " " + memory_instructions
179
114
  self._database: MongoDatabase = database
180
115
  self._accumulated_value_queue = asyncio.Queue()
181
116
  self._zep = AsyncZep(api_key=zep_api_key) if zep_api_key else None
@@ -187,94 +122,22 @@ class AI:
187
122
  Pinecone(api_key=pinecone_api_key) if pinecone_api_key else None
188
123
  )
189
124
  self._pinecone_index_name = pinecone_index_name if pinecone_index_name else None
125
+ self._pinecone_embedding_model = pinecone_embed_model
190
126
  self.kb = (
191
127
  self._pinecone.Index(
192
128
  self._pinecone_index_name) if self._pinecone else None
193
129
  )
194
- self._co = cohere.ClientV2(
195
- api_key=cohere_api_key) if cohere_api_key else None
196
- self._co_model = cohere_model if cohere_api_key else None
130
+ self._openai_base_url = openai_base_url
131
+ self._openai_model = openai_model
132
+ self._tools = []
197
133
 
198
134
  async def __aenter__(self):
199
- assistants = self._client.beta.assistants.list()
200
- existing_assistant = next(
201
- (a for a in assistants if a.name == self._name), None)
202
-
203
- if existing_assistant:
204
- self._assistant_id = existing_assistant.id
205
- else:
206
- self._assistant_id = self._client.beta.assistants.create(
207
- name=self._name,
208
- instructions=self._instructions,
209
- tools=self._tools,
210
- model=self._openai_assistant_model,
211
- ).id
212
- self._database.delete_all_threads()
213
- if self._file_search:
214
- vectore_store_id = self._database.get_vector_store_id()
215
- if vectore_store_id:
216
- self._vector_store = self._client.beta.vector_stores.retrieve(
217
- vector_store_id=vectore_store_id
218
- )
219
- else:
220
- uid = uuid.uuid4().hex
221
- self._vector_store = self._client.beta.vector_stores.create(
222
- name=uid)
223
- self._database.save_vector_store_id(self._vector_store.id)
224
- self._client.beta.assistants.update(
225
- assistant_id=self._assistant_id,
226
- tool_resources={
227
- "file_search": {"vector_store_ids": [self._vector_store.id]}
228
- },
229
- )
230
135
  return self
231
136
 
232
137
  async def __aexit__(self, exc_type, exc_val, exc_tb):
233
138
  # Perform any cleanup actions here
234
139
  pass
235
140
 
236
- async def _create_thread(self, user_id: str) -> str:
237
- thread_id = self._database.get_thread_id(user_id)
238
-
239
- if thread_id is None:
240
- thread = self._client.beta.threads.create()
241
- thread_id = thread.id
242
- self._database.save_thread_id(user_id, thread_id)
243
- if self._zep:
244
- try:
245
- await self._zep.user.add(user_id=user_id)
246
- except Exception:
247
- pass
248
- try:
249
- await self._zep.memory.add_session(
250
- user_id=user_id, session_id=user_id
251
- )
252
- except Exception:
253
- pass
254
-
255
- return thread_id
256
-
257
- async def _cancel_run(self, thread_id: str, run_id: str):
258
- try:
259
- self._client.beta.threads.runs.cancel(
260
- thread_id=thread_id, run_id=run_id)
261
- except Exception as e:
262
- print(f"Error cancelling run: {e}")
263
-
264
- async def _get_active_run(self, thread_id: str) -> Optional[str]:
265
- runs = self._client.beta.threads.runs.list(
266
- thread_id=thread_id, limit=1)
267
- for run in runs:
268
- if run.status in ["in_progress"]:
269
- return run.id
270
- return None
271
-
272
- async def _get_run_status(self, thread_id: str, run_id: str) -> str:
273
- run = self._client.beta.threads.runs.retrieve(
274
- thread_id=thread_id, run_id=run_id
275
- )
276
- return run.status
277
-
278
141
  def csv_to_text(self, file, filename: str) -> str:
279
142
  """Convert a CSV file to a Markdown table text format optimized for LLM ingestion.
280
143
 
@@ -368,20 +231,20 @@ class AI:
368
231
  self,
369
232
  file,
370
233
  filename: str,
371
- prompt: str = "Summarize the markdown table into a report, include important metrics and totals.",
234
+ id: str = uuid.uuid4().hex,
235
+ prompt: str = "Summarize the table into a report, include important metrics and totals.",
372
236
  namespace: str = "global",
373
- model: Literal["gemini-2.0-flash",
374
- "gemini-1.5-pro"] = "gemini-1.5-pro",
237
+ model: Literal["gemini-2.0-flash"] = "gemini-2.0-flash",
375
238
  ):
376
239
  """Upload and process a CSV file into the knowledge base with AI summarization.
377
240
 
378
241
  Args:
379
242
  file (BinaryIO): The CSV file to upload and process
380
243
  filename (str): The name of the CSV file
381
- prompt (str, optional): Custom prompt for summarization. Defaults to "Summarize the markdown table into a report, include important metrics and totals."
244
+ id (str, optional): Unique identifier for the document. Defaults to a random UUID.
245
+ prompt (str, optional): Custom prompt for summarization. Defaults to "Summarize the table into a report, include important metrics and totals."
382
246
  namespace (str, optional): Knowledge base namespace. Defaults to "global".
383
- model (Literal["gemini-2.0-flash", "gemini-1.5-pro"], optional):
384
- Gemini model for summarization. Defaults to "gemini-1.5-pro"
247
+ model (Literal["gemini-2.0-flash"], optional): Gemini model for summarization. Defaults to "gemini-2.0-flash".
385
248
 
386
249
  Example:
387
250
  ```python
@@ -393,123 +256,32 @@ class AI:
393
256
 
394
257
  Note:
395
258
  - Converts CSV to Markdown table format
396
- - Uses Gemini AI to generate a summary
259
+ - Uses Gemini AI to generate a summary - total of 1M context tokens
397
260
  - Stores summary in Pinecone knowledge base
398
261
  - Requires configured Pinecone index
399
262
  - Supports custom prompts for targeted summaries
400
263
  """
401
264
  csv_text = self.csv_to_text(file, filename)
402
- print(csv_text)
403
265
  document = self.summarize(csv_text, prompt, model)
404
- print(document)
405
- self.add_document_to_kb(document=document, namespace=namespace)
406
-
407
- def delete_vector_store_and_files(self):
408
- """Delete the OpenAI vector store and files.
409
-
410
- Example:
411
- ```python
412
- ai.delete_vector_store()
413
- ```
414
-
415
- Note:
416
- - Requires file_search=True in AI initialization
417
- - Deletes the vector store and all associated files
418
- """
419
- vector_store_id = self._database.get_vector_store_id()
420
- if vector_store_id:
421
- self._client.beta.vector_stores.delete(vector_store_id)
422
- self._database.delete_vector_store_id(vector_store_id)
423
- for file in self._database.files.find().to_list():
424
- self._client.files.delete(file["file_id"])
425
- self._database.delete_file(file["file_id"])
426
-
427
- def max_files(self) -> bool:
428
- """Check if the OpenAI vector store has reached its maximum file capacity.
429
-
430
- Returns:
431
- bool: True if file count is at maximum (10,000), False otherwise
432
-
433
- Example:
434
- ```python
435
- if ai.max_files():
436
- print("Vector store is full")
437
- else:
438
- print("Can still add more files")
439
- ```
440
-
441
- Note:
442
- - Requires file_search=True in AI initialization
443
- - OpenAI vector stores have a 10,000 file limit
444
- - Returns False if vector store is not configured
445
- """
446
- self._vector_store.file_counts.completed == 10000
447
-
448
- def file_count(self) -> int:
449
- """Get the total number of files processed in the OpenAI vector store.
450
-
451
- Returns:
452
- int: Number of successfully processed files in the vector store
453
-
454
- Example:
455
- ```python
456
- count = ai.file_count()
457
- print(f"Processed {count} files")
458
- # Returns: "Processed 5 files"
459
- ```
460
-
461
- Note:
462
- - Requires file_search=True in AI initialization
463
- - Only counts successfully processed files
464
- - Returns 0 if vector store is not configured
465
- """
466
- self._vector_store.file_counts.completed
467
-
468
- def add_file(
469
- self,
470
- filename: str,
471
- file_stream: bytes,
472
- ) -> Literal["in_progress", "completed", "cancelled", "failed"]:
473
- """Upload and process a file in the OpenAI vector store.
474
-
475
- Args:
476
- filename (str): Name of the file to upload
477
- file_stream (bytes): Raw bytes of the file to upload
478
-
479
- Returns:
480
- Literal["in_progress", "completed", "cancelled", "failed"]: Status of file processing
481
-
482
- Example:
483
- ```python
484
- with open('document.pdf', 'rb') as f:
485
- status = ai.add_file(f.filename, f.read())
486
- if status == "completed":
487
- print("File processed successfully")
488
- ```
489
-
490
- Note:
491
- - Requires file_search=True in AI initialization
492
- - Files are vectorized for semantic search
493
- - Maximum file size: 512MB
494
- - Maximum 10,000 files per vector store
495
- - Processing may take a few seconds to minutes
496
- """
497
- vector_store_id = self._database.get_vector_store_id()
498
- file = self._client.files.create(
499
- file=(filename, file_stream), purpose="assistants"
266
+ self.add_documents_to_kb(
267
+ documents=[DocumentModel(id=id, text=document)], namespace=namespace
500
268
  )
501
- file_batch = self._client.beta.vector_stores.files.create_and_poll(
502
- vector_store_id=vector_store_id, file_id=file.id
503
- )
504
- self._database.add_file(file.id)
505
- return file_batch.status
506
269
 
507
- def search_kb(self, query: str, namespace: str = "global", limit: int = 3) -> str:
508
- """Search Pinecone knowledge base using OpenAI embeddings.
270
+ def search_kb(
271
+ self,
272
+ query: str,
273
+ namespace: str = "global",
274
+ rerank_model: Literal["cohere-rerank-3.5"] = "cohere-rerank-3.5",
275
+ inner_limit: int = 10,
276
+ limit: int = 3,
277
+ ) -> str:
278
+ """Search Pinecone knowledge base.
509
279
 
510
280
  Args:
511
281
  query (str): Search query to find relevant documents
512
282
  namespace (str, optional): Namespace of the Pinecone to search. Defaults to "global".
283
+ rerank_model (Literal["cohere-rerank-3.5"], optional): Rerank model on Pinecone. Defaults to "cohere-rerank-3.5".
284
+ inner_limit (int, optional): Maximum number of results to rerank. Defaults to 10.
513
285
  limit (int, optional): Maximum number of results to return. Defaults to 3.
514
286
 
515
287
  Returns:
@@ -517,25 +289,23 @@ class AI:
517
289
 
518
290
  Example:
519
291
  ```python
520
- results = ai.search_kb("user123", "machine learning basics")
292
+ results = ai.search_kb("machine learning basics", "user123")
521
293
  # Returns: '["Document 1", "Document 2", ...]'
522
294
  ```
523
295
 
524
296
  Note:
525
297
  - Requires configured Pinecone index
526
- - Uses OpenAI embeddings for semantic search
527
- - Returns JSON-serialized Pinecone match metadata results
528
298
  - Returns error message string if search fails
529
- - Optionally reranks results using Cohere API
530
299
  """
531
300
  try:
532
- response = self._client.embeddings.create(
533
- input=query,
534
- model=self._openai_embedding_model,
301
+ embedding = self._pinecone.inference.embed(
302
+ model=self._pinecone_embedding_model,
303
+ inputs=[query],
304
+ parameters={"input_type": "query"},
535
305
  )
536
306
  search_results = self.kb.query(
537
- vector=response.data[0].embedding,
538
- top_k=10,
307
+ vector=embedding[0].values,
308
+ top_k=inner_limit,
539
309
  include_metadata=False,
540
310
  include_values=False,
541
311
  namespace=namespace,
@@ -548,62 +318,88 @@ class AI:
548
318
  for id in ids:
549
319
  document = self._database.kb.find_one({"reference": id})
550
320
  docs.append(document["document"])
551
- if self._co:
552
- try:
553
- response = self._co.rerank(
554
- model=self._co_model,
555
- query=query,
556
- documents=docs,
557
- top_n=limit,
558
- )
559
- reranked_docs = response.results
560
- new_docs = []
561
- for doc in reranked_docs:
562
- new_docs.append(docs[doc.index])
563
- return json.dumps(new_docs)
564
- except Exception:
565
- return json.dumps(docs[:limit])
566
- else:
321
+ try:
322
+ reranked_docs = self._pinecone.inference.rerank(
323
+ model=rerank_model,
324
+ query=query,
325
+ documents=docs,
326
+ top_n=limit,
327
+ )
328
+ new_docs = []
329
+ for doc in reranked_docs.data:
330
+ new_docs.append(docs[doc.index])
331
+ return json.dumps(new_docs)
332
+ except Exception:
567
333
  return json.dumps(docs[:limit])
568
334
  except Exception as e:
569
335
  return f"Failed to search KB. Error: {e}"
570
336
 
571
- def add_document_to_kb(
337
+ def list_documents_in_kb(self, namespace: str = "global") -> List[DocumentModel]:
338
+ """List all documents stored in the Pinecone knowledge base.
339
+
340
+ Args:
341
+ namespace (str, optional): Namespace of the Pinecone index to search. Defaults to "global".
342
+
343
+ Returns:
344
+ List[DocumentModel]: List of documents stored in the knowledge base
345
+
346
+ Example:
347
+ ```python
348
+ documents = ai.list_documents_in_kb("user123")
349
+ for doc in documents:
350
+ print(doc)
351
+ # Returns: "Document 1", "Document 2", ...
352
+ ```
353
+
354
+ Note:
355
+ - Requires Pinecone index to be configured
356
+ """
357
+ return self._database.list_documents_in_kb(namespace)
358
+
359
+ def add_documents_to_kb(
572
360
  self,
573
- document: str,
574
- id: str = uuid.uuid4().hex,
361
+ documents: List[DocumentModel],
575
362
  namespace: str = "global",
576
363
  ):
577
- """Add a document to the Pinecone knowledge base with OpenAI embeddings.
364
+ """Add documents to the Pinecone knowledge base.
578
365
 
579
366
  Args:
580
- document (str): Document to add to the knowledge base
581
- id (str, optional): Unique identifier for the document. Defaults to random UUID.
367
+ documents (List[DocumentModel]): List of documents to add to the knowledge base
582
368
  namespace (str): Namespace of the Pinecone index to search. Defaults to "global".
583
369
 
584
370
  Example:
585
371
  ```python
586
- ai.add_document_to_kb("user123 has 4 cats")
372
+ docs = [
373
+ {"id": "doc1", "text": "Document 1"},
374
+ {"id": "doc2", "text": "Document 2"},
375
+ ]
376
+ ai.add_documents_to_kb(docs, "user123")
587
377
  ```
588
378
 
589
379
  Note:
590
380
  - Requires Pinecone index to be configured
591
- - Uses OpenAI embeddings API
592
381
  """
593
- response = self._client.embeddings.create(
594
- input=document,
595
- model=self._openai_embedding_model,
382
+ embeddings = self._pinecone.inference.embed(
383
+ model=self._pinecone_embedding_model,
384
+ inputs=[d.text for d in documents],
385
+ parameters={"input_type": "passage", "truncate": "END"},
596
386
  )
597
- self.kb.upsert(
598
- vectors=[
387
+
388
+ vectors = []
389
+ for d, e in zip(documents, embeddings):
390
+ vectors.append(
599
391
  {
600
- "id": id,
601
- "values": response.data[0].embedding,
392
+ "id": d.id,
393
+ "values": e["values"],
602
394
  }
603
- ],
395
+ )
396
+
397
+ self.kb.upsert(
398
+ vectors=vectors,
604
399
  namespace=namespace,
605
400
  )
606
- self._database.add_document_to_kb(id, namespace, document)
401
+
402
+ self._database.add_documents_to_kb(namespace, documents)
607
403
 
608
404
  def delete_document_from_kb(self, id: str, user_id: str = "global"):
609
405
  """Delete a document from the Pinecone knowledge base.
@@ -644,54 +440,37 @@ class AI:
644
440
  "%Y-%m-%d %H:%M:%S %Z"
645
441
  )
646
442
 
647
- # search facts tool - has to be sync
648
- def search_facts(
443
+ # has to be sync for tool
444
+ def get_memory_context(
649
445
  self,
650
446
  user_id: str,
651
- query: str,
652
- limit: int = 10,
653
447
  ) -> str:
654
- """Search stored conversation facts using Zep memory integration.
448
+ """Retrieve contextual memory for a specific user from Zep memory storage.
655
449
 
656
450
  Args:
657
- user_id (str): Unique identifier for the user
658
- query (str): Search query to find relevant facts
659
- limit (int, optional): Maximum number of facts to return. Defaults to 10.
451
+ user_id (str): Unique identifier for the user whose memory context to retrieve
660
452
 
661
453
  Returns:
662
- str: JSON string of matched facts or error message
454
+ str: User's memory context or error message if retrieval fails
663
455
 
664
456
  Example:
665
457
  ```python
666
- facts = ai.search_facts(
667
- user_id="user123",
668
- query="How many cats do I have?"
669
- )
670
- # Returns: [{"fact": "user123 has 4 cats", "timestamp": "2022-01-01T12:00:00Z"}]
458
+ context = ai.get_memory_context("user123")
459
+ print(context)
460
+ # Returns: "User previously mentioned having 3 dogs and living in London"
671
461
  ```
672
462
 
673
463
  Note:
674
- Requires Zep integration to be configured with valid API key.
675
- This is a synchronous tool method required for OpenAI function calling.
464
+ - This is a synchronous tool method required for OpenAI function calling
465
+ - Requires Zep integration to be configured with valid API key
466
+ - Returns error message if Zep is not configured or retrieval fails
467
+ - Useful for maintaining conversation context across sessions
676
468
  """
677
- if self._sync_zep:
678
- try:
679
- facts = []
680
- results = self._sync_zep.memory.search_sessions(
681
- user_id=user_id,
682
- session_ids=[user_id],
683
- text=query,
684
- limit=limit,
685
- )
686
- for result in results.results:
687
- fact = result.fact.fact
688
- timestamp = result.fact.created_at
689
- facts.append({"fact": fact, "timestamp": timestamp})
690
- return json.dumps(facts)
691
- except Exception as e:
692
- return f"Failed to search facts. Error: {e}"
693
- else:
694
- return "Zep integration not configured."
469
+ try:
470
+ memory = self._sync_zep.memory.get(session_id=user_id)
471
+ return memory.context
472
+ except Exception:
473
+ return ""
695
474
 
696
475
  # search internet tool - has to be sync
697
476
  def search_internet(
@@ -769,7 +548,6 @@ class AI:
769
548
  prompt: str = "You combine the data with your reasoning to answer the query.",
770
549
  use_perplexity: bool = True,
771
550
  use_grok: bool = True,
772
- use_facts: bool = True,
773
551
  use_kb: bool = True,
774
552
  perplexity_model: Literal[
775
553
  "sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-reasoning"
@@ -786,7 +564,6 @@ class AI:
786
564
  prompt (str, optional): Prompt for reasoning. Defaults to "You combine the data with your reasoning to answer the query."
787
565
  use_perplexity (bool, optional): Include Perplexity search results. Defaults to True
788
566
  use_grok (bool, optional): Include X/Twitter search results. Defaults to True
789
- use_facts (bool, optional): Include stored conversation facts. Defaults to True
790
567
  use_kb (bool, optional): Include Pinecone knowledge base search results. Defaults to True
791
568
  perplexity_model (Literal, optional): Perplexity model to use. Defaults to "sonar"
792
569
  openai_model (Literal, optional): OpenAI model for reasoning. Defaults to "o3-mini"
@@ -818,13 +595,7 @@ class AI:
818
595
  kb_results = ""
819
596
  else:
820
597
  kb_results = ""
821
- if use_facts:
822
- try:
823
- facts = self.search_facts(user_id, query)
824
- except Exception:
825
- facts = ""
826
- else:
827
- facts = ""
598
+
828
599
  if use_perplexity:
829
600
  try:
830
601
  search_results = self.search_internet(
@@ -833,6 +604,7 @@ class AI:
833
604
  search_results = ""
834
605
  else:
835
606
  search_results = ""
607
+
836
608
  if use_grok:
837
609
  try:
838
610
  x_search_results = self.search_x(query, grok_model)
@@ -841,6 +613,11 @@ class AI:
841
613
  else:
842
614
  x_search_results = ""
843
615
 
616
+ if self._zep:
617
+ memory = self._sync_zep.memory.get(session_id=user_id)
618
+ else:
619
+ memory = ""
620
+
844
621
  response = self._client.chat.completions.create(
845
622
  model=openai_model,
846
623
  messages=[
@@ -850,7 +627,7 @@ class AI:
850
627
  },
851
628
  {
852
629
  "role": "user",
853
- "content": f"Query: {query}, Facts: {facts}, KB Results: {kb_results}, Internet Search Results: {search_results}, X Search Results: {x_search_results}",
630
+ "content": f"Query: {query}, Memory: {memory}, KB Results: {kb_results}, Internet Search Results: {search_results}, X Search Results: {x_search_results}",
854
631
  },
855
632
  ],
856
633
  )
@@ -916,16 +693,12 @@ class AI:
916
693
  Note:
917
694
  This is an async method and must be awaited.
918
695
  """
919
- try:
920
- await self.delete_assistant_thread(user_id)
921
- except Exception:
922
- pass
923
696
  try:
924
697
  self._database.clear_user_history(user_id)
925
698
  except Exception:
926
699
  pass
927
700
  try:
928
- await self.delete_facts(user_id)
701
+ await self.delete_memory(user_id)
929
702
  except Exception:
930
703
  pass
931
704
 
@@ -941,8 +714,8 @@ class AI:
941
714
  thread_id = self._database.get_thread_id(user_id)
942
715
  await self._client.beta.threads.delete(thread_id=thread_id)
943
716
 
944
- async def delete_facts(self, user_id: str):
945
- """Delete stored conversation facts for a specific user from Zep memory.
717
+ async def delete_memory(self, user_id: str):
718
+ """Delete memory for a specific user from Zep memory.
946
719
 
947
720
  Args:
948
721
  user_id (str): Unique identifier for the user whose facts should be deleted
@@ -973,11 +746,11 @@ class AI:
973
746
  """Process text input and stream AI responses asynchronously.
974
747
 
975
748
  Args:
976
- user_id (str): Unique identifier for the user/conversation
977
- user_text (str): Text input from user to process
749
+ user_id (str): Unique identifier for the user/conversation.
750
+ user_text (str): Text input from user to process.
978
751
 
979
752
  Returns:
980
- AsyncGenerator[str, None]: Stream of response text chunks
753
+ AsyncGenerator[str, None]: Stream of response text chunks (including tool call results).
981
754
 
982
755
  Example:
983
756
  ```python
@@ -986,69 +759,109 @@ class AI:
986
759
  ```
987
760
 
988
761
  Note:
989
- - Maintains conversation thread using OpenAI's thread system
990
- - Stores messages in configured database (MongoDB/SQLite)
991
- - Integrates with Zep memory if configured
992
- - Handles concurrent runs by canceling active ones
993
- - Streams responses for real-time interaction
762
+ - Maintains conversation thread using OpenAI's thread system.
763
+ - Stores messages in configured database (MongoDB/SQLite).
764
+ - Integrates with Zep memory if configured.
765
+ - Supports tool calls by aggregating and executing them as their arguments stream in.
994
766
  """
995
767
  self._accumulated_value_queue = asyncio.Queue()
996
-
997
- thread_id = self._database.get_thread_id(user_id)
998
-
999
- if thread_id is None:
1000
- thread_id = await self._create_thread(user_id)
1001
-
1002
- self._current_thread_id = thread_id
1003
-
1004
- # Check for active runs and cancel if necessary
1005
- active_run_id = await self._get_active_run(thread_id)
1006
- if active_run_id:
1007
- await self._cancel_run(thread_id, active_run_id)
1008
- while await self._get_run_status(thread_id, active_run_id) != "cancelled":
1009
- await asyncio.sleep(0.1)
1010
-
1011
- # Create a message in the thread
1012
- self._client.beta.threads.messages.create(
1013
- thread_id=thread_id,
1014
- role="user",
1015
- content=user_text,
1016
- )
1017
- event_handler = EventHandler(self._tool_handlers, self)
768
+ final_tool_calls = {} # Accumulate tool call deltas
769
+ final_response = ""
1018
770
 
1019
771
  async def stream_processor():
1020
- with self._client.beta.threads.runs.stream(
1021
- thread_id=thread_id,
1022
- assistant_id=self._assistant_id,
1023
- event_handler=event_handler,
1024
- ) as stream:
1025
- stream.until_done()
1026
-
1027
- # Start the stream processor in a separate task
772
+ memory = self.get_memory_context(user_id)
773
+ response = self._client.chat.completions.create(
774
+ model=self._openai_model,
775
+ messages=[
776
+ {
777
+ "role": "system",
778
+ "content": self._instructions + f" Memory: {memory}",
779
+
780
+ },
781
+ {
782
+ "role": "user",
783
+ "content": user_text,
784
+ },
785
+ ],
786
+ tools=self._tools,
787
+ stream=True,
788
+ )
789
+ for chunk in response:
790
+ delta = chunk.choices[0].delta
791
+
792
+ # Process tool call deltas (if any)
793
+ if delta.tool_calls:
794
+ for tool_call in delta.tool_calls:
795
+ index = tool_call.index
796
+ if tool_call.function.name:
797
+ # Initialize a new tool call record
798
+ final_tool_calls[index] = {
799
+ "name": tool_call.function.name,
800
+ "arguments": tool_call.function.arguments or ""
801
+ }
802
+ elif tool_call.function.arguments:
803
+ # Append additional arguments if provided in subsequent chunks
804
+ final_tool_calls[index]["arguments"] += tool_call.function.arguments
805
+
806
+ try:
807
+ args = json.loads(
808
+ final_tool_calls[index]["arguments"])
809
+ func = getattr(
810
+ self, final_tool_calls[index]["name"])
811
+ # Execute the tool call (synchronously; adjust if async is needed)
812
+ result = func(**args)
813
+ response = self._client.chat.completions.create(
814
+ model=self._openai_model,
815
+ messages=[
816
+ {
817
+ "role": "system",
818
+ "content": self._instructions,
819
+ },
820
+ {
821
+ "role": "user",
822
+ "content": result,
823
+ },
824
+ ],
825
+ tools=self._tools,
826
+ stream=True,
827
+ )
828
+ for chunk in response:
829
+ delta = chunk.choices[0].delta
830
+
831
+ if delta.content is not None:
832
+ await self._accumulated_value_queue.put(delta.content)
833
+ # Remove the cached tool call info so it doesn't block future calls
834
+ del final_tool_calls[index]
835
+ except json.JSONDecodeError:
836
+ # If the accumulated arguments aren't valid yet, wait for more chunks.
837
+ continue
838
+
839
+ # Process regular response content
840
+ if delta.content is not None:
841
+ await self._accumulated_value_queue.put(delta.content)
842
+
843
+ # Start the stream processor as a background task
1028
844
  asyncio.create_task(stream_processor())
1029
845
 
1030
- # Yield values from the queue as they become available
1031
- full_response = ""
846
+ # Yield values from the queue as they become available.
1032
847
  while True:
1033
848
  try:
1034
- value = await asyncio.wait_for(
1035
- self._accumulated_value_queue.get(), timeout=0.1
1036
- )
849
+ value = await asyncio.wait_for(self._accumulated_value_queue.get(), timeout=0.1)
1037
850
  if value is not None:
1038
- full_response += value
851
+ final_response += value
1039
852
  yield value
1040
853
  except asyncio.TimeoutError:
854
+ # Break only if the queue is empty (assuming stream ended)
1041
855
  if self._accumulated_value_queue.empty():
1042
856
  break
1043
857
 
1044
- # Save the message to the database
858
+ # Save the conversation to the database and Zep memory (if configured)
1045
859
  metadata = {
1046
860
  "user_id": user_id,
1047
861
  "message": user_text,
1048
- "response": full_response,
862
+ "response": final_response,
1049
863
  "timestamp": datetime.datetime.now(datetime.timezone.utc),
1050
864
  }
1051
-
1052
865
  self._database.save_message(user_id, metadata)
1053
866
  if self._zep:
1054
867
  messages = [
@@ -1060,7 +873,7 @@ class AI:
1060
873
  Message(
1061
874
  role="assistant",
1062
875
  role_type="assistant",
1063
- content=full_response,
876
+ content=final_response,
1064
877
  ),
1065
878
  ]
1066
879
  await self._zep.memory.add(session_id=user_id, messages=messages)
@@ -1112,56 +925,105 @@ class AI:
1112
925
  - Streams audio response using OpenAI TTS
1113
926
  """
1114
927
 
1115
- # Reset the queue for each new conversation
1116
- self._accumulated_value_queue = asyncio.Queue()
1117
-
1118
- thread_id = self._database.get_thread_id(user_id)
1119
-
1120
- if thread_id is None:
1121
- thread_id = await self._create_thread(user_id)
1122
-
1123
- self._current_thread_id = thread_id
1124
928
  transcript = await self._listen(audio_bytes, input_format)
1125
- event_handler = EventHandler(self._tool_handlers, self)
1126
- self._client.beta.threads.messages.create(
1127
- thread_id=thread_id,
1128
- role="user",
1129
- content=transcript,
1130
- )
929
+ self._accumulated_value_queue = asyncio.Queue()
930
+ final_tool_calls = {} # Accumulate tool call deltas
931
+ final_response = ""
1131
932
 
1132
933
  async def stream_processor():
1133
- with self._client.beta.threads.runs.stream(
1134
- thread_id=thread_id,
1135
- assistant_id=self._assistant_id,
1136
- event_handler=event_handler,
1137
- ) as stream:
1138
- stream.until_done()
1139
-
1140
- # Start the stream processor in a separate task
934
+ memory = self.get_memory_context(user_id)
935
+ response = self._client.chat.completions.create(
936
+ model=self._openai_model,
937
+ messages=[
938
+ {
939
+ "role": "system",
940
+ "content": self._instructions + f" Memory: {memory}",
941
+ },
942
+ {
943
+ "role": "user",
944
+ "content": transcript,
945
+ },
946
+ ],
947
+ tools=self._tools,
948
+ stream=True,
949
+ )
950
+ for chunk in response:
951
+ delta = chunk.choices[0].delta
952
+
953
+ # Process tool call deltas (if any)
954
+ if delta.tool_calls:
955
+ for tool_call in delta.tool_calls:
956
+ index = tool_call.index
957
+ if tool_call.function.name:
958
+ # Initialize a new tool call record
959
+ final_tool_calls[index] = {
960
+ "name": tool_call.function.name,
961
+ "arguments": tool_call.function.arguments or ""
962
+ }
963
+ elif tool_call.function.arguments:
964
+ # Append additional arguments if provided in subsequent chunks
965
+ final_tool_calls[index]["arguments"] += tool_call.function.arguments
966
+
967
+ try:
968
+ args = json.loads(
969
+ final_tool_calls[index]["arguments"])
970
+ func = getattr(
971
+ self, final_tool_calls[index]["name"])
972
+ # Execute the tool call (synchronously; adjust if async is needed)
973
+ result = func(**args)
974
+ response = self._client.chat.completions.create(
975
+ model=self._openai_model,
976
+ messages=[
977
+ {
978
+ "role": "system",
979
+ "content": self._instructions,
980
+ },
981
+ {
982
+ "role": "user",
983
+ "content": result,
984
+ },
985
+ ],
986
+ tools=self._tools,
987
+ stream=True,
988
+ )
989
+ for chunk in response:
990
+ delta = chunk.choices[0].delta
991
+
992
+ if delta.content is not None:
993
+ await self._accumulated_value_queue.put(delta.content)
994
+ # Remove the cached tool call info so it doesn't block future calls
995
+ del final_tool_calls[index]
996
+ except json.JSONDecodeError:
997
+ # If the accumulated arguments aren't valid yet, wait for more chunks.
998
+ continue
999
+
1000
+ # Process regular response content
1001
+ if delta.content is not None:
1002
+ await self._accumulated_value_queue.put(delta.content)
1003
+
1004
+ # Start the stream processor as a background task
1141
1005
  asyncio.create_task(stream_processor())
1142
1006
 
1143
- # Collect the full response
1144
- full_response = ""
1007
+ # Yield values from the queue as they become available.
1145
1008
  while True:
1146
1009
  try:
1147
- value = await asyncio.wait_for(
1148
- self._accumulated_value_queue.get(), timeout=0.1
1149
- )
1010
+ value = await asyncio.wait_for(self._accumulated_value_queue.get(), timeout=0.1)
1150
1011
  if value is not None:
1151
- full_response += value
1012
+ final_response += value
1013
+ yield value
1152
1014
  except asyncio.TimeoutError:
1015
+ # Break only if the queue is empty (assuming stream ended)
1153
1016
  if self._accumulated_value_queue.empty():
1154
1017
  break
1155
1018
 
1019
+ # Save the conversation to the database and Zep memory (if configured)
1156
1020
  metadata = {
1157
1021
  "user_id": user_id,
1158
1022
  "message": transcript,
1159
- "response": full_response,
1023
+ "response": final_response,
1160
1024
  "timestamp": datetime.datetime.now(datetime.timezone.utc),
1161
1025
  }
1162
-
1163
1026
  self._database.save_message(user_id, metadata)
1164
-
1165
1027
  if self._zep:
1166
1028
  messages = [
1167
1029
  Message(
@@ -1172,7 +1034,7 @@ class AI:
1172
1034
  Message(
1173
1035
  role="assistant",
1174
1036
  role_type="assistant",
1175
- content=full_response,
1037
+ content=final_response,
1176
1038
  ),
1177
1039
  ]
1178
1040
  await self._zep.memory.add(session_id=user_id, messages=messages)
@@ -1181,32 +1043,12 @@ class AI:
1181
1043
  with self._client.audio.speech.with_streaming_response.create(
1182
1044
  model="tts-1",
1183
1045
  voice=voice,
1184
- input=full_response,
1046
+ input=final_response,
1185
1047
  response_format=response_format,
1186
1048
  ) as response:
1187
1049
  for chunk in response.iter_bytes(1024):
1188
1050
  yield chunk
1189
1051
 
1190
- def _handle_requires_action(self, data, run_id):
1191
- tool_outputs = []
1192
-
1193
- for tool in data.required_action.submit_tool_outputs.tool_calls:
1194
- if tool.function.name in self._tool_handlers:
1195
- handler = self._tool_handlers[tool.function.name]
1196
- inputs = json.loads(tool.function.arguments)
1197
- output = handler(**inputs)
1198
- tool_outputs.append(
1199
- {"tool_call_id": tool.id, "output": output})
1200
-
1201
- self._submit_tool_outputs(tool_outputs, run_id)
1202
-
1203
- def _submit_tool_outputs(self, tool_outputs, run_id):
1204
- with self._client.beta.threads.runs.submit_tool_outputs_stream(
1205
- thread_id=self._current_thread_id, run_id=run_id, tool_outputs=tool_outputs
1206
- ) as stream:
1207
- for text in stream.text_deltas:
1208
- asyncio.create_task(self._accumulated_value_queue.put(text))
1209
-
1210
1052
  def add_tool(self, func: Callable):
1211
1053
  """Register a custom function as an AI tool using decorator pattern.
1212
1054
 
@@ -1253,7 +1095,8 @@ class AI:
1253
1095
  },
1254
1096
  }
1255
1097
  self._tools.append(tool_config)
1256
- self._tool_handlers[func.__name__] = func
1098
+ # Attach the function to the instance so getattr can find it later.
1099
+ setattr(self, func.__name__, func)
1257
1100
  return func
1258
1101
 
1259
1102
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: solana-agent
3
- Version: 1.4.4
3
+ Version: 2.0.1
4
4
  Summary: Build self-learning AI Agents
5
5
  License: MIT
6
6
  Keywords: ai,openai,ai agents
@@ -16,8 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
- Requires-Dist: cohere (>=5.13.12,<6.0.0)
20
- Requires-Dist: openai (>=1.63.2,<2.0.0)
19
+ Requires-Dist: openai (>=1.64.0,<2.0.0)
21
20
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
22
21
  Requires-Dist: pinecone (>=6.0.1,<7.0.0)
23
22
  Requires-Dist: pydantic (>=2.10.6,<3.0.0)
@@ -58,16 +57,15 @@ Unlike traditional AI assistants that forget conversations after each session, S
58
57
  - Real-time voice-to-voice conversations
59
58
 
60
59
  🧠 **Memory System and Extensibility**
61
- - Advanced AI memory combining conversational context, conversational facts, knowledge base, file search, and parallel tool calling
60
+ - Advanced AI memory combining conversational context, knowledge base, and parallel tool calling
62
61
  - Create custom tools for extending the Agent's capabilities like further API integrations
63
62
 
64
63
  🔍 **Multi-Source Search and Reasoning**
65
64
  - Internet search via Perplexity
66
65
  - X (Twitter) search using Grok
67
- - Conversational fact search powered by Zep
66
+ - Conversational memory powered by Zep
68
67
  - Conversational message history using MongoDB (on-prem or hosted)
69
- - Knowledge Base (KB) using Pinecone with reranking by Cohere - available globally or user-specific
70
- - File uploading and searching using OpenAI like for PDFs
68
+ - Knowledge Base (KB) using Pinecone with reranking - available globally or user-specific
71
69
  - Upload CSVs to be processed into summary reports and stored in the Knowledge Base (KB) using Gemini
72
70
  - Comprehensive reasoning combining multiple data sources
73
71
 
@@ -80,7 +78,7 @@ Unlike traditional AI assistants that forget conversations after each session, S
80
78
  - Persistent cross-session knowledge retention
81
79
  - Automatic self-learning from conversations
82
80
  - Knowledge Base to add domain specific knowledge
83
- - File uploads to perform document context search
81
+ - CSV file uploads to perform document context search
84
82
 
85
83
  🏢 **Enterprise Focus**
86
84
  - Production-ready out of the box in a few lines of code
@@ -0,0 +1,6 @@
1
+ solana_agent/__init__.py,sha256=zpfnWqANd3OHGWm7NCF5Y6m01BWG4NkNk8SK9Ex48nA,18
2
+ solana_agent/ai.py,sha256=h2Lo-TpZSNEBc61Vn_uQj1_nVjL4g-PBNruJz0daiYY,43502
3
+ solana_agent-2.0.1.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
4
+ solana_agent-2.0.1.dist-info/METADATA,sha256=jXaJPbeUj1TPyDylj5DrW2_SlChpiczArOdr_4qlneQ,4726
5
+ solana_agent-2.0.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
6
+ solana_agent-2.0.1.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- solana_agent/__init__.py,sha256=zpfnWqANd3OHGWm7NCF5Y6m01BWG4NkNk8SK9Ex48nA,18
2
- solana_agent/ai.py,sha256=AF_d589vUT1OyzSXLQIi6ea2nLCCCHqA7hAAzQ0Z6mg,47053
3
- solana_agent-1.4.4.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
4
- solana_agent-1.4.4.dist-info/METADATA,sha256=X9xePqXmZ1ioFss8I0w9K-G8H9aJJJv1tXQSJhODdLQ,4871
5
- solana_agent-1.4.4.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
6
- solana_agent-1.4.4.dist-info/RECORD,,