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 +7 -2
- solana_agent/adapters/openai_adapter.py +17 -21
- solana_agent/factories/agent_factory.py +60 -79
- solana_agent/guardrails/pii.py +107 -0
- solana_agent/interfaces/guardrails/guardrails.py +26 -0
- solana_agent/interfaces/providers/llm.py +1 -1
- solana_agent/services/agent.py +577 -308
- solana_agent/services/query.py +140 -58
- {solana_agent-27.4.3.dist-info → solana_agent-28.0.0.dist-info}/METADATA +108 -52
- {solana_agent-27.4.3.dist-info → solana_agent-28.0.0.dist-info}/RECORD +12 -10
- {solana_agent-27.4.3.dist-info → solana_agent-28.0.0.dist-info}/LICENSE +0 -0
- {solana_agent-27.4.3.dist-info → solana_agent-28.0.0.dist-info}/WHEEL +0 -0
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
|
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
|
-
) ->
|
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":
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
167
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|