solana-agent 27.1.0__py3-none-any.whl → 27.3.0__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.
@@ -6,11 +6,12 @@ the agent system without dealing with internal implementation details.
6
6
  """
7
7
  import json
8
8
  import importlib.util
9
- from typing import AsyncGenerator, Dict, Any, Literal, Optional, Union
9
+ from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Union
10
10
 
11
11
  from solana_agent.factories.agent_factory import SolanaAgentFactory
12
12
  from solana_agent.interfaces.client.client import SolanaAgent as SolanaAgentInterface
13
13
  from solana_agent.interfaces.plugins.plugins import Tool
14
+ from solana_agent.services.knowledge_base import KnowledgeBaseService
14
15
  from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
15
16
 
16
17
 
@@ -135,3 +136,146 @@ class SolanaAgent(SolanaAgentInterface):
135
136
  self.query_service.agent_service.assign_tool_for_agent(
136
137
  agent_name, tool.name)
137
138
  return success
139
+
140
+ def _ensure_kb(self) -> KnowledgeBaseService:
141
+ """Checks if the knowledge base service is available and returns it."""
142
+ if hasattr(self.query_service, 'knowledge_base') and self.query_service.knowledge_base:
143
+ return self.query_service.knowledge_base
144
+ else:
145
+ raise AttributeError(
146
+ "Knowledge base service not configured or available.")
147
+
148
+ async def kb_add_document(
149
+ self,
150
+ text: str,
151
+ metadata: Dict[str, Any],
152
+ document_id: Optional[str] = None,
153
+ namespace: Optional[str] = None
154
+ ) -> str:
155
+ """
156
+ Add a document to the knowledge base.
157
+
158
+ Args:
159
+ text: Document text content.
160
+ metadata: Document metadata.
161
+ document_id: Optional document ID.
162
+ namespace: Optional Pinecone namespace.
163
+
164
+ Returns:
165
+ The document ID.
166
+ """
167
+ kb = self._ensure_kb()
168
+ return await kb.add_document(text, metadata, document_id, namespace)
169
+
170
+ async def kb_query(
171
+ self,
172
+ query_text: str,
173
+ filter: Optional[Dict[str, Any]] = None,
174
+ top_k: int = 5,
175
+ namespace: Optional[str] = None,
176
+ include_content: bool = True,
177
+ include_metadata: bool = True
178
+ ) -> List[Dict[str, Any]]:
179
+ """
180
+ Query the knowledge base.
181
+
182
+ Args:
183
+ query_text: Search query text.
184
+ filter: Optional filter criteria.
185
+ top_k: Maximum number of results.
186
+ namespace: Optional Pinecone namespace.
187
+ include_content: Include document content in results.
188
+ include_metadata: Include document metadata in results.
189
+
190
+ Returns:
191
+ List of matching documents.
192
+ """
193
+ kb = self._ensure_kb()
194
+ return await kb.query(query_text, filter, top_k, namespace, include_content, include_metadata)
195
+
196
+ async def kb_delete_document(
197
+ self,
198
+ document_id: str,
199
+ namespace: Optional[str] = None
200
+ ) -> bool:
201
+ """
202
+ Delete a document from the knowledge base.
203
+
204
+ Args:
205
+ document_id: ID of document to delete.
206
+ namespace: Optional Pinecone namespace.
207
+
208
+ Returns:
209
+ True if successful.
210
+ """
211
+ kb = self._ensure_kb()
212
+ return await kb.delete_document(document_id, namespace)
213
+
214
+ async def kb_update_document(
215
+ self,
216
+ document_id: str,
217
+ text: Optional[str] = None,
218
+ metadata: Optional[Dict[str, Any]] = None,
219
+ namespace: Optional[str] = None
220
+ ) -> bool:
221
+ """
222
+ Update an existing document in the knowledge base.
223
+
224
+ Args:
225
+ document_id: ID of document to update.
226
+ text: Optional new text content.
227
+ metadata: Optional metadata to update.
228
+ namespace: Optional Pinecone namespace.
229
+
230
+ Returns:
231
+ True if successful.
232
+ """
233
+ kb = self._ensure_kb()
234
+ return await kb.update_document(document_id, text, metadata, namespace)
235
+
236
+ async def kb_add_documents_batch(
237
+ self,
238
+ documents: List[Dict[str, Any]],
239
+ namespace: Optional[str] = None,
240
+ batch_size: int = 50
241
+ ) -> List[str]:
242
+ """
243
+ Add multiple documents to the knowledge base in batches.
244
+
245
+ Args:
246
+ documents: List of documents ({'text': ..., 'metadata': ...}).
247
+ namespace: Optional Pinecone namespace.
248
+ batch_size: Number of documents per batch.
249
+
250
+ Returns:
251
+ List of added document IDs.
252
+ """
253
+ kb = self._ensure_kb()
254
+ return await kb.add_documents_batch(documents, namespace, batch_size)
255
+
256
+ async def kb_add_pdf_document(
257
+ self,
258
+ pdf_data: Union[bytes, str],
259
+ metadata: Dict[str, Any],
260
+ document_id: Optional[str] = None,
261
+ namespace: Optional[str] = None,
262
+ chunk_batch_size: int = 50
263
+ ) -> str:
264
+ """
265
+ Add a PDF document to the knowledge base via the client.
266
+
267
+ Args:
268
+ pdf_data: PDF content as bytes or a path to the PDF file.
269
+ metadata: Document metadata.
270
+ document_id: Optional parent document ID.
271
+ namespace: Optional Pinecone namespace for chunks.
272
+ chunk_batch_size: Batch size for upserting chunks.
273
+
274
+ Returns:
275
+ The parent document ID.
276
+ """
277
+ kb = self._ensure_kb()
278
+ # Type check added for clarity, though handled in service
279
+ if not isinstance(pdf_data, (bytes, str)):
280
+ raise TypeError("pdf_data must be bytes or a file path string.")
281
+ return await kb.add_pdf_document(pdf_data, metadata, document_id, namespace, chunk_batch_size)
@@ -7,15 +7,17 @@ services and components used in the system.
7
7
  from typing import Dict, Any
8
8
 
9
9
  # Service imports
10
+ from solana_agent.adapters.pinecone_adapter import PineconeAdapter
10
11
  from solana_agent.services.query import QueryService
11
12
  from solana_agent.services.agent import AgentService
12
13
  from solana_agent.services.routing import RoutingService
14
+ from solana_agent.services.knowledge_base import KnowledgeBaseService
13
15
 
14
16
  # Repository imports
15
17
  from solana_agent.repositories.memory import MemoryRepository
16
18
 
17
19
  # Adapter imports
18
- from solana_agent.adapters.llm_adapter import OpenAIAdapter
20
+ from solana_agent.adapters.openai_adapter import OpenAIAdapter
19
21
  from solana_agent.adapters.mongodb_adapter import MongoDBAdapter
20
22
 
21
23
  # Domain and plugin imports
@@ -39,11 +41,6 @@ class SolanaAgentFactory:
39
41
  # Create adapters
40
42
 
41
43
  if "mongo" in config:
42
- # MongoDB connection string and database name
43
- if "connection_string" not in config["mongo"]:
44
- raise ValueError("MongoDB connection string is required.")
45
- if "database" not in config["mongo"]:
46
- raise ValueError("MongoDB database name is required.")
47
44
  db_adapter = MongoDBAdapter(
48
45
  connection_string=config["mongo"]["connection_string"],
49
46
  database_name=config["mongo"]["database"],
@@ -71,8 +68,6 @@ class SolanaAgentFactory:
71
68
  memory_provider = None
72
69
 
73
70
  if "zep" in config and "mongo" in config:
74
- if "api_key" not in config["zep"]:
75
- raise ValueError("Zep API key is required.")
76
71
  memory_provider = MemoryRepository(
77
72
  mongo_adapter=db_adapter, zep_api_key=config["zep"].get("api_key"))
78
73
 
@@ -198,11 +193,82 @@ class SolanaAgentFactory:
198
193
  agent_service.assign_tool_for_agent(
199
194
  agent_name, tool_name)
200
195
 
196
+ # Initialize Knowledge Base if configured
197
+ knowledge_base = None
198
+ kb_config = config.get("knowledge_base")
199
+ # Requires both KB config section and MongoDB adapter
200
+ if kb_config and db_adapter:
201
+ try:
202
+ pinecone_config = kb_config.get("pinecone", {})
203
+ splitter_config = kb_config.get("splitter", {})
204
+ # Get OpenAI embedding config (used by KBService)
205
+ openai_embed_config = kb_config.get("openai_embeddings", {})
206
+
207
+ # Determine OpenAI model and dimensions for KBService
208
+ openai_model_name = openai_embed_config.get(
209
+ "model_name", "text-embedding-3-large")
210
+ if openai_model_name == "text-embedding-3-large":
211
+ openai_dimensions = 3072
212
+ elif openai_model_name == "text-embedding-3-small": # pragma: no cover
213
+ openai_dimensions = 1536 # pragma: no cover
214
+
215
+ # Create Pinecone adapter for KB
216
+ # It now relies on external embeddings, so dimension MUST match OpenAI model
217
+ pinecone_adapter = PineconeAdapter(
218
+ api_key=pinecone_config.get("api_key"),
219
+ index_name=pinecone_config.get("index_name"),
220
+ # This dimension MUST match the OpenAI model used by KBService
221
+ embedding_dimensions=openai_dimensions,
222
+ cloud_provider=pinecone_config.get(
223
+ "cloud_provider", "aws"),
224
+ region=pinecone_config.get("region", "us-east-1"),
225
+ metric=pinecone_config.get("metric", "cosine"),
226
+ create_index_if_not_exists=pinecone_config.get(
227
+ "create_index", True),
228
+ # Reranking config
229
+ use_reranking=pinecone_config.get("use_reranking", False),
230
+ rerank_model=pinecone_config.get("rerank_model"),
231
+ rerank_top_k=pinecone_config.get("rerank_top_k", 3),
232
+ initial_query_top_k_multiplier=pinecone_config.get(
233
+ "initial_query_top_k_multiplier", 5),
234
+ rerank_text_field=pinecone_config.get(
235
+ "rerank_text_field", "text"),
236
+ )
237
+
238
+ # Create the KB service using OpenAI embeddings
239
+ knowledge_base = KnowledgeBaseService(
240
+ pinecone_adapter=pinecone_adapter,
241
+ mongodb_adapter=db_adapter,
242
+ # Pass OpenAI config directly
243
+ openai_api_key=openai_embed_config.get("api_key") or config.get("openai", {}).get(
244
+ "api_key"),
245
+ openai_model_name=openai_model_name,
246
+ collection_name=kb_config.get(
247
+ "collection_name", "knowledge_documents"),
248
+ # Pass rerank config (though PineconeAdapter handles the logic)
249
+ rerank_results=pinecone_config.get("use_reranking", False),
250
+ rerank_top_k=pinecone_config.get("rerank_top_k", 3),
251
+ # Pass splitter config
252
+ splitter_buffer_size=splitter_config.get("buffer_size", 1),
253
+ splitter_breakpoint_percentile=splitter_config.get(
254
+ "breakpoint_percentile", 95)
255
+ )
256
+ print("Knowledge Base Service initialized successfully.")
257
+
258
+ except Exception as e:
259
+ print(f"Failed to initialize Knowledge Base: {e}")
260
+ import traceback
261
+ print(traceback.format_exc())
262
+ knowledge_base = None # Ensure KB is None if init fails
263
+
201
264
  # Create and return the query service
202
265
  query_service = QueryService(
203
266
  agent_service=agent_service,
204
267
  routing_service=routing_service,
205
268
  memory_provider=memory_provider,
269
+ knowledge_base=knowledge_base, # Pass the potentially created KB
270
+ kb_results_count=kb_config.get(
271
+ "results_count", 3) if kb_config else 3
206
272
  )
207
273
 
208
274
  return query_service
@@ -1,12 +1,11 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, AsyncGenerator, Dict, Literal, Optional, Union
3
-
2
+ from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Union
4
3
  from solana_agent.interfaces.plugins.plugins import Tool
5
4
  from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
6
5
 
7
6
 
8
7
  class SolanaAgent(ABC):
9
- """Interface for the Solana agent system."""
8
+ """Interface for the Solana Agent client."""
10
9
 
11
10
  @abstractmethod
12
11
  async def process(
@@ -28,18 +27,89 @@ class SolanaAgent(ABC):
28
27
  """Process a user message and return the response stream."""
29
28
  pass
30
29
 
30
+ @abstractmethod
31
+ async def delete_user_history(self, user_id: str) -> None:
32
+ """Delete the conversation history for a user."""
33
+ pass
34
+
31
35
  @abstractmethod
32
36
  async def get_user_history(
33
37
  self,
34
38
  user_id: str,
35
39
  page_num: int = 1,
36
40
  page_size: int = 20,
37
- sort_order: str = "desc" # "asc" for oldest-first, "desc" for newest-first
41
+ sort_order: str = "desc"
38
42
  ) -> Dict[str, Any]:
39
43
  """Get paginated message history for a user."""
40
44
  pass
41
45
 
42
46
  @abstractmethod
43
47
  def register_tool(self, agent_name: str, tool: Tool) -> bool:
44
- """Register a tool in the registry."""
48
+ """Register a tool with the agent system."""
49
+ pass
50
+
51
+ @abstractmethod
52
+ async def kb_add_document(
53
+ self,
54
+ text: str,
55
+ metadata: Dict[str, Any],
56
+ document_id: Optional[str] = None,
57
+ namespace: Optional[str] = None
58
+ ) -> str:
59
+ """Add a document to the knowledge base."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ async def kb_query(
64
+ self,
65
+ query_text: str,
66
+ filter: Optional[Dict[str, Any]] = None,
67
+ top_k: int = 5,
68
+ namespace: Optional[str] = None,
69
+ include_content: bool = True,
70
+ include_metadata: bool = True
71
+ ) -> List[Dict[str, Any]]:
72
+ """Query the knowledge base."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ async def kb_delete_document(
77
+ self,
78
+ document_id: str,
79
+ namespace: Optional[str] = None
80
+ ) -> bool:
81
+ """Delete a document from the knowledge base."""
82
+ pass
83
+
84
+ @abstractmethod
85
+ async def kb_update_document(
86
+ self,
87
+ document_id: str,
88
+ text: Optional[str] = None,
89
+ metadata: Optional[Dict[str, Any]] = None,
90
+ namespace: Optional[str] = None
91
+ ) -> bool:
92
+ """Update an existing document in the knowledge base."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ async def kb_add_documents_batch(
97
+ self,
98
+ documents: List[Dict[str, Any]],
99
+ namespace: Optional[str] = None,
100
+ batch_size: int = 50
101
+ ) -> List[str]:
102
+ """Add multiple documents to the knowledge base in batches."""
103
+ pass
104
+
105
+ @abstractmethod
106
+ async def kb_add_pdf_document(
107
+ self,
108
+ pdf_data: Union[bytes, str],
109
+ metadata: Dict[str, Any],
110
+ document_id: Optional[str] = None,
111
+ namespace: Optional[str] = None,
112
+ chunk_batch_size: int = 50
113
+ ) -> str:
114
+ """Add a PDF document to the knowledge base."""
45
115
  pass
@@ -57,3 +57,23 @@ class LLMProvider(ABC):
57
57
  ) -> AsyncGenerator[str, None]:
58
58
  """Transcribe audio from the language model."""
59
59
  pass
60
+
61
+ @abstractmethod
62
+ async def embed_text(
63
+ self,
64
+ text: str,
65
+ model: Optional[str] = None,
66
+ dimensions: Optional[int] = None
67
+ ) -> List[float]:
68
+ """
69
+ Generate an embedding for the given text.
70
+
71
+ Args:
72
+ text: The text to embed.
73
+ model: The embedding model to use.
74
+ dimensions: Optional desired output dimensions for the embedding.
75
+
76
+ Returns:
77
+ A list of floats representing the embedding vector.
78
+ """
79
+ pass
@@ -0,0 +1,59 @@
1
+ from typing import List, Dict, Any, Optional
2
+
3
+
4
+ class VectorStorageProvider:
5
+ """Interface for Vector Storage Providers."""
6
+
7
+ async def upsert(
8
+ self,
9
+ vectors: List[Dict[str, Any]],
10
+ namespace: Optional[str] = None
11
+ ) -> None:
12
+ """Upsert vectors into the storage."""
13
+ pass
14
+
15
+ async def upsert_text(
16
+ self,
17
+ texts: List[str],
18
+ ids: List[str],
19
+ metadatas: Optional[List[Dict[str, Any]]] = None,
20
+ namespace: Optional[str] = None
21
+ ) -> None:
22
+ """Embeds texts and upserts them into the storage."""
23
+ pass
24
+
25
+ async def query(
26
+ self,
27
+ vector: List[float],
28
+ top_k: int = 5,
29
+ namespace: Optional[str] = None,
30
+ filter: Optional[Dict[str, Any]] = None,
31
+ include_values: bool = False,
32
+ include_metadata: bool = True,
33
+ ) -> List[Dict[str, Any]]:
34
+ """Query for similar vectors."""
35
+ pass
36
+
37
+ async def query_text(
38
+ self,
39
+ query_text: str,
40
+ top_k: Optional[int] = None,
41
+ namespace: Optional[str] = None,
42
+ filter: Optional[Dict[str, Any]] = None,
43
+ include_values: bool = False,
44
+ include_metadata: bool = True,
45
+ ) -> List[Dict[str, Any]]:
46
+ """Embeds query text and queries the storage."""
47
+ pass
48
+
49
+ async def delete(
50
+ self,
51
+ ids: List[str],
52
+ namespace: Optional[str] = None
53
+ ) -> None:
54
+ """Delete vectors by IDs."""
55
+ pass
56
+
57
+ async def describe_index_stats(self) -> Dict[str, Any]:
58
+ """Get statistics about the index/collection."""
59
+ pass
@@ -0,0 +1,86 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Any, Optional, Union
3
+
4
+
5
+ class KnowledgeBaseService(ABC):
6
+ """
7
+ Interface for a Knowledge Base service.
8
+ """
9
+
10
+ @abstractmethod
11
+ async def add_document(
12
+ self,
13
+ text: str,
14
+ metadata: Dict[str, Any],
15
+ document_id: Optional[str] = None,
16
+ namespace: Optional[str] = None
17
+ ) -> str:
18
+ """
19
+ Add a document to the knowledge base.
20
+ """
21
+ pass
22
+
23
+ @abstractmethod
24
+ async def query(
25
+ self,
26
+ query_text: str,
27
+ filter: Optional[Dict[str, Any]] = None,
28
+ top_k: int = 5,
29
+ namespace: Optional[str] = None,
30
+ include_content: bool = True,
31
+ include_metadata: bool = True
32
+ ) -> List[Dict[str, Any]]:
33
+ """
34
+ Query the knowledge base with semantic search.
35
+ """
36
+ pass
37
+
38
+ @abstractmethod
39
+ async def delete_document(
40
+ self,
41
+ document_id: str,
42
+ namespace: Optional[str] = None
43
+ ) -> bool:
44
+ """
45
+ Delete a document from the knowledge base.
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ async def update_document(
51
+ self,
52
+ document_id: str,
53
+ text: Optional[str] = None,
54
+ metadata: Optional[Dict[str, Any]] = None,
55
+ namespace: Optional[str] = None
56
+ ) -> bool:
57
+ """
58
+ Update an existing document in the knowledge base.
59
+ """
60
+ pass
61
+
62
+ @abstractmethod
63
+ async def add_documents_batch(
64
+ self,
65
+ documents: List[Dict[str, Any]],
66
+ namespace: Optional[str] = None,
67
+ batch_size: int = 50
68
+ ) -> List[str]:
69
+ """
70
+ Add multiple documents in batches.
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ async def add_pdf_document(
76
+ self,
77
+ pdf_data: Union[bytes, str], # PDF bytes or file path
78
+ metadata: Dict[str, Any],
79
+ document_id: Optional[str] = None,
80
+ namespace: Optional[str] = None,
81
+ chunk_batch_size: int = 50
82
+ ) -> str:
83
+ """
84
+ Add a PDF document to the knowledge base.
85
+ """
86
+ pass