solana-agent 27.4.3__py3-none-any.whl → 28.0.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.
solana_agent/__init__.py CHANGED
@@ -5,8 +5,6 @@ This package provides a modular framework for building AI agent systems with
5
5
  multiple specialized agents, memory management, and conversation routing.
6
6
  """
7
7
 
8
- __version__ = "14.0.0" # Update with your actual version
9
-
10
8
  # Client interface (main entry point)
11
9
  from solana_agent.client.solana_agent import SolanaAgent
12
10
 
@@ -17,6 +15,10 @@ from solana_agent.factories.agent_factory import SolanaAgentFactory
17
15
  from solana_agent.plugins.manager import PluginManager
18
16
  from solana_agent.plugins.registry import ToolRegistry
19
17
  from solana_agent.plugins.tools.auto_tool import AutoTool
18
+ from solana_agent.interfaces.guardrails.guardrails import (
19
+ InputGuardrail,
20
+ OutputGuardrail,
21
+ )
20
22
 
21
23
  # Package metadata
22
24
  __all__ = [
@@ -28,4 +30,7 @@ __all__ = [
28
30
  "PluginManager",
29
31
  "ToolRegistry",
30
32
  "AutoTool",
33
+ # Guardrails
34
+ "InputGuardrail",
35
+ "OutputGuardrail",
31
36
  ]
@@ -15,7 +15,7 @@ from solana_agent.interfaces.providers.llm import LLMProvider
15
15
 
16
16
  T = TypeVar("T", bound=BaseModel)
17
17
 
18
- DEFAULT_CHAT_MODEL = "gpt-4.1-mini"
18
+ DEFAULT_CHAT_MODEL = "gpt-4.1"
19
19
  DEFAULT_PARSE_MODEL = "gpt-4.1-nano"
20
20
  DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
21
21
  DEFAULT_EMBEDDING_DIMENSIONS = 3072
@@ -129,45 +129,41 @@ class OpenAIAdapter(LLMProvider):
129
129
  api_key: Optional[str] = None,
130
130
  base_url: Optional[str] = None,
131
131
  model: Optional[str] = None,
132
- ) -> AsyncGenerator[str, None]: # pragma: no cover
133
- """Generate text from OpenAI models."""
132
+ ) -> str: # pragma: no cover
133
+ """Generate text from OpenAI models as a single string."""
134
134
  messages = []
135
-
136
135
  if system_prompt:
137
136
  messages.append({"role": "system", "content": system_prompt})
138
-
139
137
  messages.append({"role": "user", "content": prompt})
140
138
 
141
- # Prepare request parameters
139
+ # Prepare request parameters - stream is always False now
142
140
  request_params = {
143
141
  "messages": messages,
144
- "stream": True,
145
- "model": self.text_model,
142
+ "stream": False, # Hardcoded to False
143
+ "model": model or self.text_model,
146
144
  }
147
145
 
146
+ # Determine client based on provided api_key/base_url
148
147
  if api_key and base_url:
149
148
  client = AsyncOpenAI(api_key=api_key, base_url=base_url)
150
149
  else:
151
150
  client = self.client
152
151
 
153
- if model:
154
- request_params["model"] = model
155
-
156
152
  try:
153
+ # Make the non-streaming API call
157
154
  response = await client.chat.completions.create(**request_params)
158
155
 
159
- async for chunk in response:
160
- if chunk.choices:
161
- if chunk.choices[0].delta.content:
162
- text = chunk.choices[0].delta.content
163
- yield text
156
+ # Handle non-streaming response
157
+ if response.choices and response.choices[0].message.content:
158
+ full_text = response.choices[0].message.content
159
+ return full_text # Return the complete string
160
+ else:
161
+ print("Received non-streaming response with no content.")
162
+ return "" # Return empty string if no content
164
163
 
165
164
  except Exception as e:
166
- print(f"Error in generate_text: {str(e)}")
167
- import traceback
168
-
169
- print(traceback.format_exc())
170
- yield f"I apologize, but I encountered an error: {str(e)}"
165
+ # Log the error and return an error message string
166
+ print(f"Error in generate_text: {e}")
171
167
 
172
168
  async def parse_structured_output(
173
169
  self,
@@ -5,10 +5,15 @@ This module handles the creation and dependency injection for all
5
5
  services and components used in the system.
6
6
  """
7
7
 
8
- from typing import Dict, Any
8
+ import importlib
9
+ from typing import Dict, Any, List
9
10
 
10
11
  # Service imports
11
12
  from solana_agent.adapters.pinecone_adapter import PineconeAdapter
13
+ from solana_agent.interfaces.guardrails.guardrails import (
14
+ InputGuardrail,
15
+ OutputGuardrail,
16
+ )
12
17
  from solana_agent.services.query import QueryService
13
18
  from solana_agent.services.agent import AgentService
14
19
  from solana_agent.services.routing import RoutingService
@@ -29,6 +34,37 @@ from solana_agent.plugins.manager import PluginManager
29
34
  class SolanaAgentFactory:
30
35
  """Factory for creating and wiring components of the Solana Agent system."""
31
36
 
37
+ @staticmethod
38
+ def _create_guardrails(guardrail_configs: List[Dict[str, Any]]) -> List[Any]:
39
+ """Instantiates guardrails from configuration."""
40
+ guardrails = []
41
+ if not guardrail_configs:
42
+ return guardrails
43
+
44
+ for config in guardrail_configs:
45
+ class_path = config.get("class")
46
+ guardrail_config = config.get("config", {})
47
+ if not class_path:
48
+ print(f"Guardrail config missing 'class': {config}")
49
+ continue
50
+ try:
51
+ module_path, class_name = class_path.rsplit(".", 1)
52
+ module = importlib.import_module(module_path)
53
+ guardrail_class = getattr(module, class_name)
54
+ # Instantiate the guardrail, handling potential errors during init
55
+ try:
56
+ guardrails.append(guardrail_class(config=guardrail_config))
57
+ print(f"Successfully loaded guardrail: {class_path}")
58
+ except Exception as init_e:
59
+ print(f"Error initializing guardrail '{class_path}': {init_e}")
60
+ # Optionally re-raise or just skip this guardrail
61
+
62
+ except (ImportError, AttributeError, ValueError) as e:
63
+ print(f"Error loading guardrail class '{class_path}': {e}")
64
+ except Exception as e: # Catch unexpected errors during import/getattr
65
+ print(f"Unexpected error loading guardrail '{class_path}': {e}")
66
+ return guardrails
67
+
32
68
  @staticmethod
33
69
  def create_from_config(config: Dict[str, Any]) -> QueryService:
34
70
  """Create the agent system from configuration.
@@ -83,86 +119,30 @@ class SolanaAgentFactory:
83
119
  raise ValueError("Zep API key is required.")
84
120
  memory_provider = MemoryRepository(zep_api_key=config["zep"].get("api_key"))
85
121
 
86
- if (
87
- "gemini" in config
88
- and "api_key" in config["gemini"]
89
- and "grok" not in config
90
- ):
91
- # Create primary services
92
- agent_service = AgentService(
93
- llm_provider=llm_adapter,
94
- business_mission=business_mission,
95
- config=config,
96
- api_key=config["gemini"]["api_key"],
97
- base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
98
- model="gemini-2.5-flash-preview-04-17",
99
- )
100
-
101
- # Create routing service
102
- routing_service = RoutingService(
103
- llm_provider=llm_adapter,
104
- agent_service=agent_service,
105
- api_key=config["gemini"]["api_key"],
106
- base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
107
- model="gemini-2.5-flash-preview-04-17",
108
- )
109
-
110
- elif (
111
- "gemini" in config
112
- and "api_key" in config["gemini"]
113
- and "grok" in config
114
- and "api_key" in config["grok"]
115
- ):
116
- # Create primary services
117
- agent_service = AgentService(
118
- llm_provider=llm_adapter,
119
- business_mission=business_mission,
120
- config=config,
121
- api_key=config["grok"]["api_key"],
122
- base_url="https://api.x.ai/v1",
123
- model="grok-3-mini-fast-beta",
124
- )
125
- # Create routing service
126
- routing_service = RoutingService(
127
- llm_provider=llm_adapter,
128
- agent_service=agent_service,
129
- api_key=config["gemini"]["api_key"],
130
- base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
131
- model="gemini-2.5-flash-preview-04-17",
132
- )
133
-
134
- elif (
135
- "grok" in config and "api_key" in config["grok"] and "gemini" not in config
136
- ):
137
- # Create primary services
138
- agent_service = AgentService(
139
- llm_provider=llm_adapter,
140
- business_mission=business_mission,
141
- config=config,
142
- api_key=config["grok"]["api_key"],
143
- base_url="https://api.x.ai/v1",
144
- model="grok-3-mini-fast-beta",
145
- )
146
-
147
- # Create routing service
148
- routing_service = RoutingService(
149
- llm_provider=llm_adapter,
150
- agent_service=agent_service,
151
- )
122
+ guardrail_config = config.get("guardrails", {})
123
+ input_guardrails: List[InputGuardrail] = SolanaAgentFactory._create_guardrails(
124
+ guardrail_config.get("input", [])
125
+ )
126
+ output_guardrails: List[OutputGuardrail] = (
127
+ SolanaAgentFactory._create_guardrails(guardrail_config.get("output", []))
128
+ )
129
+ print(
130
+ f"Loaded {len(input_guardrails)} input guardrails and {len(output_guardrails)} output guardrails."
131
+ )
152
132
 
153
- else:
154
- # Create primary services
155
- agent_service = AgentService(
156
- llm_provider=llm_adapter,
157
- business_mission=business_mission,
158
- config=config,
159
- )
133
+ # Create primary services
134
+ agent_service = AgentService(
135
+ llm_provider=llm_adapter,
136
+ business_mission=business_mission,
137
+ config=config,
138
+ output_guardrails=output_guardrails,
139
+ )
160
140
 
161
- # Create routing service
162
- routing_service = RoutingService(
163
- llm_provider=llm_adapter,
164
- agent_service=agent_service,
165
- )
141
+ # Create routing service
142
+ routing_service = RoutingService(
143
+ llm_provider=llm_adapter,
144
+ agent_service=agent_service,
145
+ )
166
146
 
167
147
  # Debug the agent service tool registry
168
148
  print(
@@ -284,6 +264,7 @@ class SolanaAgentFactory:
284
264
  memory_provider=memory_provider,
285
265
  knowledge_base=knowledge_base, # Pass the potentially created KB
286
266
  kb_results_count=kb_config.get("results_count", 3) if kb_config else 3,
267
+ input_guardrails=input_guardrails,
287
268
  )
288
269
 
289
270
  return query_service
@@ -0,0 +1,107 @@
1
+ import logging
2
+ from typing import Dict, Any, Optional, List
3
+ import scrubadub
4
+ from solana_agent.interfaces.guardrails.guardrails import (
5
+ InputGuardrail,
6
+ OutputGuardrail,
7
+ )
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PII(InputGuardrail, OutputGuardrail):
13
+ """
14
+ A guardrail using Scrubadub to detect and remove PII.
15
+
16
+ Requires 'scrubadub'. Install with: pip install solana-agent[guardrails]
17
+ """
18
+
19
+ DEFAULT_REPLACEMENT = "[REDACTED_{detector_name}]"
20
+ DEFAULT_LANG = "en_US" # Scrubadub uses locale format
21
+
22
+ def __init__(self, config: Dict[str, Any] = None):
23
+ super().__init__(config)
24
+ self.replacement_format = self.config.get(
25
+ "replacement", self.DEFAULT_REPLACEMENT
26
+ )
27
+ self.locale = self.config.get("locale", self.DEFAULT_LANG)
28
+ # Optional: Specify detectors to use, None uses defaults
29
+ self.detector_list: Optional[List[str]] = self.config.get("detectors")
30
+ # Optional: Add custom detectors if needed via config
31
+ self.extra_detector_list = self.config.get(
32
+ "extra_detectors", []
33
+ ) # List of detector classes/instances
34
+
35
+ try:
36
+ # Initialize Scrubber
37
+ # Note: detector_list expects instances, not names. Need mapping or direct instantiation if customizing.
38
+ # For simplicity, we'll use defaults or allow passing instances via config (advanced).
39
+ # Using default detectors if self.detector_list is None.
40
+ if self.detector_list is not None:
41
+ logger.warning(
42
+ "Customizing 'detectors' by name list is not directly supported here yet. Using defaults."
43
+ )
44
+ # TODO: Add logic to map names to detector classes if needed.
45
+ self.scrubber = scrubadub.Scrubber(locale=self.locale)
46
+ else:
47
+ self.scrubber = scrubadub.Scrubber(locale=self.locale)
48
+
49
+ # Add any extra detectors passed via config (e.g., custom regex detectors)
50
+ for detector in self.extra_detector_list:
51
+ # Assuming extra_detectors are already instantiated objects
52
+ # Or add logic here to instantiate them based on class paths/names
53
+ if isinstance(detector, scrubadub.detectors.Detector):
54
+ self.scrubber.add_detector(detector)
55
+ else:
56
+ logger.warning(f"Invalid item in extra_detectors: {detector}")
57
+
58
+ logger.info(f"ScrubadubPIIFilter initialized for locale '{self.locale}'")
59
+
60
+ except ImportError:
61
+ logger.error(
62
+ "Scrubadub not installed. Please install with 'pip install solana-agent[guardrails]'"
63
+ )
64
+ raise
65
+ except Exception as e:
66
+ logger.error(f"Failed to initialize Scrubadub: {e}", exc_info=True)
67
+ raise
68
+
69
+ async def process(self, text: str) -> str:
70
+ """Clean text using Scrubadub."""
71
+ try:
72
+ # Scrubadub's clean method handles the replacement logic.
73
+ # We need to customize the replacement format per detector.
74
+ # This requires iterating through filth found first.
75
+
76
+ clean_text = text
77
+ filth_list = list(self.scrubber.iter_filth(text)) # Get all findings
78
+
79
+ if not filth_list:
80
+ return text
81
+
82
+ # Sort by start index to handle replacements correctly
83
+ filth_list.sort(key=lambda f: f.beg)
84
+
85
+ offset = 0
86
+ for filth in filth_list:
87
+ start = filth.beg + offset
88
+ end = filth.end + offset
89
+ replacement_text = self.replacement_format.format(
90
+ detector_name=filth.detector_name,
91
+ text=filth.text,
92
+ locale=filth.locale,
93
+ # Add other filth attributes if needed in format string
94
+ )
95
+
96
+ clean_text = clean_text[:start] + replacement_text + clean_text[end:]
97
+ offset += len(replacement_text) - (filth.end - filth.beg)
98
+
99
+ if clean_text != text:
100
+ logger.debug(
101
+ f"ScrubadubPIIFilter redacted {len(filth_list)} pieces of filth."
102
+ )
103
+ return clean_text
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error during Scrubadub cleaning: {e}", exc_info=True)
107
+ return text # Return original text on error
@@ -0,0 +1,26 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+
5
+ class Guardrail(ABC):
6
+ """Base class for all guardrails."""
7
+
8
+ def __init__(self, config: Dict[str, Any] = None):
9
+ self.config = config or {}
10
+
11
+ @abstractmethod
12
+ async def process(self, text: str) -> str:
13
+ """Process the text and return the modified text."""
14
+ pass
15
+
16
+
17
+ class InputGuardrail(Guardrail):
18
+ """Interface for guardrails applied to user input."""
19
+
20
+ pass
21
+
22
+
23
+ class OutputGuardrail(Guardrail):
24
+ """Interface for guardrails applied to agent output."""
25
+
26
+ pass
@@ -25,7 +25,7 @@ class LLMProvider(ABC):
25
25
  api_key: Optional[str] = None,
26
26
  base_url: Optional[str] = None,
27
27
  model: Optional[str] = None,
28
- ) -> AsyncGenerator[str, None]:
28
+ ) -> str:
29
29
  """Generate text from the language model."""
30
30
  pass
31
31