solana-agent 20.1.2__py3-none-any.whl → 31.4.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 +10 -5
- solana_agent/adapters/ffmpeg_transcoder.py +375 -0
- solana_agent/adapters/mongodb_adapter.py +15 -2
- solana_agent/adapters/openai_adapter.py +679 -0
- solana_agent/adapters/openai_realtime_ws.py +1813 -0
- solana_agent/adapters/pinecone_adapter.py +543 -0
- solana_agent/cli.py +128 -0
- solana_agent/client/solana_agent.py +180 -20
- solana_agent/domains/agent.py +13 -13
- solana_agent/domains/routing.py +18 -8
- solana_agent/factories/agent_factory.py +239 -38
- solana_agent/guardrails/pii.py +107 -0
- solana_agent/interfaces/client/client.py +95 -12
- solana_agent/interfaces/guardrails/guardrails.py +26 -0
- solana_agent/interfaces/plugins/plugins.py +2 -1
- solana_agent/interfaces/providers/__init__.py +0 -0
- solana_agent/interfaces/providers/audio.py +40 -0
- solana_agent/interfaces/providers/data_storage.py +9 -2
- solana_agent/interfaces/providers/llm.py +86 -9
- solana_agent/interfaces/providers/memory.py +13 -1
- solana_agent/interfaces/providers/realtime.py +212 -0
- solana_agent/interfaces/providers/vector_storage.py +53 -0
- solana_agent/interfaces/services/agent.py +27 -12
- solana_agent/interfaces/services/knowledge_base.py +59 -0
- solana_agent/interfaces/services/query.py +41 -8
- solana_agent/interfaces/services/routing.py +0 -1
- solana_agent/plugins/manager.py +37 -16
- solana_agent/plugins/registry.py +34 -19
- solana_agent/plugins/tools/__init__.py +0 -5
- solana_agent/plugins/tools/auto_tool.py +1 -0
- solana_agent/repositories/memory.py +332 -111
- solana_agent/services/__init__.py +1 -1
- solana_agent/services/agent.py +390 -241
- solana_agent/services/knowledge_base.py +768 -0
- solana_agent/services/query.py +1858 -153
- solana_agent/services/realtime.py +626 -0
- solana_agent/services/routing.py +104 -51
- solana_agent-31.4.0.dist-info/METADATA +1070 -0
- solana_agent-31.4.0.dist-info/RECORD +49 -0
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
- solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
- solana_agent/adapters/llm_adapter.py +0 -160
- solana_agent-20.1.2.dist-info/METADATA +0 -464
- solana_agent-20.1.2.dist-info/RECORD +0 -35
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
solana_agent/services/agent.py
CHANGED
|
@@ -4,17 +4,26 @@ Agent service implementation.
|
|
|
4
4
|
This service manages AI and human agents, their registration, tool assignments,
|
|
5
5
|
and response generation.
|
|
6
6
|
"""
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import datetime as main_datetime
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
import json
|
|
11
|
-
|
|
11
|
+
import logging # Add logging
|
|
12
|
+
import re
|
|
13
|
+
from typing import AsyncGenerator, Dict, List, Literal, Optional, Any, Type, Union
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
12
16
|
|
|
13
17
|
from solana_agent.interfaces.services.agent import AgentService as AgentServiceInterface
|
|
14
18
|
from solana_agent.interfaces.providers.llm import LLMProvider
|
|
15
19
|
from solana_agent.plugins.manager import PluginManager
|
|
16
20
|
from solana_agent.plugins.registry import ToolRegistry
|
|
17
21
|
from solana_agent.domains.agent import AIAgent, BusinessMission
|
|
22
|
+
from solana_agent.interfaces.guardrails.guardrails import (
|
|
23
|
+
OutputGuardrail,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__) # Add logger
|
|
18
27
|
|
|
19
28
|
|
|
20
29
|
class AgentService(AgentServiceInterface):
|
|
@@ -25,6 +34,12 @@ class AgentService(AgentServiceInterface):
|
|
|
25
34
|
llm_provider: LLMProvider,
|
|
26
35
|
business_mission: Optional[BusinessMission] = None,
|
|
27
36
|
config: Optional[Dict[str, Any]] = None,
|
|
37
|
+
api_key: Optional[str] = None,
|
|
38
|
+
base_url: Optional[str] = None,
|
|
39
|
+
model: Optional[str] = None,
|
|
40
|
+
output_guardrails: List[
|
|
41
|
+
OutputGuardrail
|
|
42
|
+
] = None, # <-- Add output_guardrails parameter
|
|
28
43
|
):
|
|
29
44
|
"""Initialize the agent service.
|
|
30
45
|
|
|
@@ -32,6 +47,10 @@ class AgentService(AgentServiceInterface):
|
|
|
32
47
|
llm_provider: Provider for language model interactions
|
|
33
48
|
business_mission: Optional business mission and values
|
|
34
49
|
config: Optional service configuration
|
|
50
|
+
api_key: API key for the LLM provider
|
|
51
|
+
base_url: Base URL for the LLM provider
|
|
52
|
+
model: Model name for the LLM provider
|
|
53
|
+
output_guardrails: List of output guardrail instances
|
|
35
54
|
"""
|
|
36
55
|
self.llm_provider = llm_provider
|
|
37
56
|
self.business_mission = business_mission
|
|
@@ -39,6 +58,10 @@ class AgentService(AgentServiceInterface):
|
|
|
39
58
|
self.last_text_response = ""
|
|
40
59
|
self.tool_registry = ToolRegistry(config=self.config)
|
|
41
60
|
self.agents: List[AIAgent] = []
|
|
61
|
+
self.api_key = api_key
|
|
62
|
+
self.base_url = base_url
|
|
63
|
+
self.model = model
|
|
64
|
+
self.output_guardrails = output_guardrails or [] # <-- Store guardrails
|
|
42
65
|
|
|
43
66
|
self.plugin_manager = PluginManager(
|
|
44
67
|
config=self.config,
|
|
@@ -46,7 +69,12 @@ class AgentService(AgentServiceInterface):
|
|
|
46
69
|
)
|
|
47
70
|
|
|
48
71
|
def register_ai_agent(
|
|
49
|
-
self,
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
instructions: str,
|
|
75
|
+
specialization: str,
|
|
76
|
+
capture_name: Optional[str] = None,
|
|
77
|
+
capture_schema: Optional[Dict[str, Any]] = None,
|
|
50
78
|
) -> None:
|
|
51
79
|
"""Register an AI agent with its specialization.
|
|
52
80
|
|
|
@@ -59,8 +87,11 @@ class AgentService(AgentServiceInterface):
|
|
|
59
87
|
name=name,
|
|
60
88
|
instructions=instructions,
|
|
61
89
|
specialization=specialization,
|
|
90
|
+
capture_name=capture_name,
|
|
91
|
+
capture_schema=capture_schema,
|
|
62
92
|
)
|
|
63
93
|
self.agents.append(agent)
|
|
94
|
+
logger.info(f"Registered AI agent: {name}")
|
|
64
95
|
|
|
65
96
|
def get_agent_system_prompt(self, agent_name: str) -> str:
|
|
66
97
|
"""Get the system prompt for an agent.
|
|
@@ -71,7 +102,6 @@ class AgentService(AgentServiceInterface):
|
|
|
71
102
|
Returns:
|
|
72
103
|
System prompt
|
|
73
104
|
"""
|
|
74
|
-
|
|
75
105
|
# Get agent by name
|
|
76
106
|
agent = next((a for a in self.agents if a.name == agent_name), None)
|
|
77
107
|
|
|
@@ -88,20 +118,52 @@ class AgentService(AgentServiceInterface):
|
|
|
88
118
|
system_prompt += f"\n\nVOICE OF THE BRAND:\n{self.business_mission.voice}"
|
|
89
119
|
|
|
90
120
|
if self.business_mission.values:
|
|
91
|
-
values_text = "\n".join(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
values_text = "\n".join(
|
|
122
|
+
[
|
|
123
|
+
f"- {value.get('name', '')}: {value.get('description', '')}"
|
|
124
|
+
for value in self.business_mission.values
|
|
125
|
+
]
|
|
126
|
+
)
|
|
95
127
|
system_prompt += f"\n\nBUSINESS VALUES:\n{values_text}"
|
|
96
128
|
|
|
97
129
|
# Add goals if available
|
|
98
130
|
if self.business_mission.goals:
|
|
99
131
|
goals_text = "\n".join(
|
|
100
|
-
[f"- {goal}" for goal in self.business_mission.goals]
|
|
132
|
+
[f"- {goal}" for goal in self.business_mission.goals]
|
|
133
|
+
)
|
|
101
134
|
system_prompt += f"\n\nBUSINESS GOALS:\n{goals_text}"
|
|
102
135
|
|
|
136
|
+
# Add capture guidance if this agent has a capture schema
|
|
137
|
+
if getattr(agent, "capture_schema", None) and getattr(
|
|
138
|
+
agent, "capture_name", None
|
|
139
|
+
): # pragma: no cover
|
|
140
|
+
system_prompt += (
|
|
141
|
+
"\n\nSTRUCTURED DATA CAPTURE:\n"
|
|
142
|
+
f"You must collect the following fields for the form '{agent.capture_name}'. "
|
|
143
|
+
"Ask concise follow-up questions to fill any missing required fields one at a time. "
|
|
144
|
+
"Confirm values when ambiguous, and summarize the captured data before finalizing.\n\n"
|
|
145
|
+
"JSON Schema (authoritative definition of the fields):\n"
|
|
146
|
+
f"{agent.capture_schema}\n\n"
|
|
147
|
+
"Rules:\n"
|
|
148
|
+
"- Never invent values—ask the user.\n"
|
|
149
|
+
"- Validate types (emails look like emails, numbers are numbers, booleans are yes/no).\n"
|
|
150
|
+
"- If the user declines to provide a required value, note it clearly.\n"
|
|
151
|
+
"- When all required fields are provided, acknowledge completion.\n"
|
|
152
|
+
)
|
|
153
|
+
|
|
103
154
|
return system_prompt
|
|
104
155
|
|
|
156
|
+
def get_agent_capture(
|
|
157
|
+
self, agent_name: str
|
|
158
|
+
) -> Optional[Dict[str, Any]]: # pragma: no cover
|
|
159
|
+
"""Return capture metadata for the agent, if any."""
|
|
160
|
+
agent = next((a for a in self.agents if a.name == agent_name), None)
|
|
161
|
+
if not agent:
|
|
162
|
+
return None
|
|
163
|
+
if agent.capture_name and agent.capture_schema:
|
|
164
|
+
return {"name": agent.capture_name, "schema": agent.capture_schema}
|
|
165
|
+
return None
|
|
166
|
+
|
|
105
167
|
def get_all_ai_agents(self) -> Dict[str, AIAgent]:
|
|
106
168
|
"""Get all registered AI agents.
|
|
107
169
|
|
|
@@ -133,31 +195,47 @@ class AgentService(AgentServiceInterface):
|
|
|
133
195
|
"""
|
|
134
196
|
return self.tool_registry.get_agent_tools(agent_name)
|
|
135
197
|
|
|
136
|
-
async def execute_tool(
|
|
198
|
+
async def execute_tool(
|
|
199
|
+
self, agent_name: str, tool_name: str, parameters: Dict[str, Any]
|
|
200
|
+
) -> Dict[str, Any]:
|
|
137
201
|
"""Execute a tool on behalf of an agent."""
|
|
138
202
|
|
|
139
203
|
if not self.tool_registry:
|
|
204
|
+
logger.error("Tool registry not available during tool execution.")
|
|
140
205
|
return {"status": "error", "message": "Tool registry not available"}
|
|
141
206
|
|
|
142
207
|
tool = self.tool_registry.get_tool(tool_name)
|
|
143
208
|
if not tool:
|
|
209
|
+
logger.warning(f"Tool '{tool_name}' not found for execution.")
|
|
144
210
|
return {"status": "error", "message": f"Tool '{tool_name}' not found"}
|
|
145
211
|
|
|
146
212
|
# Check if agent has access to this tool
|
|
147
213
|
agent_tools = self.tool_registry.get_agent_tools(agent_name)
|
|
148
214
|
|
|
149
215
|
if not any(t.get("name") == tool_name for t in agent_tools):
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"Agent '{agent_name}' attempted to use unassigned tool '{tool_name}'."
|
|
218
|
+
)
|
|
150
219
|
return {
|
|
151
220
|
"status": "error",
|
|
152
|
-
"message": f"Agent '{agent_name}' doesn't have access to tool '{tool_name}'"
|
|
221
|
+
"message": f"Agent '{agent_name}' doesn't have access to tool '{tool_name}'",
|
|
153
222
|
}
|
|
154
223
|
|
|
155
224
|
try:
|
|
225
|
+
logger.info(
|
|
226
|
+
f"Executing tool '{tool_name}' for agent '{agent_name}' with params: {parameters}"
|
|
227
|
+
)
|
|
156
228
|
result = await tool.execute(**parameters)
|
|
229
|
+
logger.info(
|
|
230
|
+
f"Tool '{tool_name}' execution result status: {result.get('status')}"
|
|
231
|
+
)
|
|
157
232
|
return result
|
|
158
233
|
except Exception as e:
|
|
159
234
|
import traceback
|
|
160
|
-
|
|
235
|
+
|
|
236
|
+
logger.error(
|
|
237
|
+
f"Error executing tool '{tool_name}': {e}\n{traceback.format_exc()}"
|
|
238
|
+
)
|
|
161
239
|
return {"status": "error", "message": f"Error executing tool: {str(e)}"}
|
|
162
240
|
|
|
163
241
|
async def generate_response(
|
|
@@ -165,259 +243,330 @@ class AgentService(AgentServiceInterface):
|
|
|
165
243
|
agent_name: str,
|
|
166
244
|
user_id: str,
|
|
167
245
|
query: Union[str, bytes],
|
|
246
|
+
images: Optional[List[Union[str, bytes]]] = None,
|
|
168
247
|
memory_context: str = "",
|
|
169
248
|
output_format: Literal["text", "audio"] = "text",
|
|
170
|
-
audio_voice: Literal[
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"
|
|
177
|
-
|
|
249
|
+
audio_voice: Literal[
|
|
250
|
+
"alloy",
|
|
251
|
+
"ash",
|
|
252
|
+
"ballad",
|
|
253
|
+
"coral",
|
|
254
|
+
"echo",
|
|
255
|
+
"fable",
|
|
256
|
+
"onyx",
|
|
257
|
+
"nova",
|
|
258
|
+
"sage",
|
|
259
|
+
"shimmer",
|
|
260
|
+
] = "nova",
|
|
261
|
+
audio_output_format: Literal[
|
|
262
|
+
"mp3", "opus", "aac", "flac", "wav", "pcm"
|
|
263
|
+
] = "aac",
|
|
178
264
|
prompt: Optional[str] = None,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
agent_name: Agent name
|
|
184
|
-
user_id: User ID
|
|
185
|
-
query: Text query or audio bytes
|
|
186
|
-
memory_context: Optional conversation context
|
|
187
|
-
output_format: Response format ("text" or "audio")
|
|
188
|
-
audio_voice: Voice to use for audio output
|
|
189
|
-
audio_instructions: Optional instructions for audio synthesis
|
|
190
|
-
audio_output_format: Audio output format
|
|
191
|
-
audio_input_format: Audio input format
|
|
192
|
-
prompt: Optional prompt for the agent
|
|
193
|
-
|
|
194
|
-
Yields:
|
|
195
|
-
Text chunks or audio bytes depending on output_format
|
|
196
|
-
"""
|
|
197
|
-
agent = next((a for a in self.agents if a.name == agent_name), None)
|
|
198
|
-
if not agent:
|
|
199
|
-
error_msg = f"Agent '{agent_name}' not found."
|
|
200
|
-
if output_format == "audio":
|
|
201
|
-
async for chunk in self.llm_provider.tts(error_msg, instructions=audio_instructions, response_format=audio_output_format, voice=audio_voice):
|
|
202
|
-
yield chunk
|
|
203
|
-
else:
|
|
204
|
-
yield error_msg
|
|
205
|
-
return
|
|
265
|
+
output_model: Optional[Type[BaseModel]] = None,
|
|
266
|
+
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]: # pragma: no cover
|
|
267
|
+
"""Generate a response using tool-calling with full streaming support."""
|
|
206
268
|
|
|
207
269
|
try:
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
if not
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
270
|
+
# Validate agent
|
|
271
|
+
agent = next((a for a in self.agents if a.name == agent_name), None)
|
|
272
|
+
if not agent:
|
|
273
|
+
error_msg = f"Agent '{agent_name}' not found."
|
|
274
|
+
logger.warning(error_msg)
|
|
275
|
+
if output_format == "audio":
|
|
276
|
+
async for chunk in self.llm_provider.tts(
|
|
277
|
+
error_msg,
|
|
278
|
+
response_format=audio_output_format,
|
|
279
|
+
voice=audio_voice,
|
|
280
|
+
):
|
|
281
|
+
yield chunk
|
|
282
|
+
else:
|
|
283
|
+
yield error_msg
|
|
284
|
+
return
|
|
215
285
|
|
|
216
|
-
#
|
|
286
|
+
# Build system prompt and messages
|
|
217
287
|
system_prompt = self.get_agent_system_prompt(agent_name)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
system_prompt = f"{system_prompt}\n\n{tool_usage_prompt}"
|
|
288
|
+
user_content = str(query)
|
|
289
|
+
if images:
|
|
290
|
+
user_content += "\n\n[Images attached]"
|
|
222
291
|
|
|
223
|
-
#
|
|
224
|
-
|
|
292
|
+
# Compose the prompt for generate_text
|
|
293
|
+
full_prompt = ""
|
|
225
294
|
if memory_context:
|
|
226
|
-
|
|
295
|
+
full_prompt += f"CONVERSATION HISTORY:\n{memory_context}\n\n Always use your tools to perform actions and don't rely on your memory!\n\n"
|
|
227
296
|
if prompt:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
):
|
|
274
|
-
processed_text += processed_chunk
|
|
275
|
-
# For text output, yield chunks as they come
|
|
276
|
-
if output_format == "text":
|
|
277
|
-
yield processed_chunk
|
|
278
|
-
|
|
279
|
-
# Add to complete response
|
|
280
|
-
complete_text_response += processed_text
|
|
281
|
-
|
|
282
|
-
# For audio output, process the complete text
|
|
283
|
-
if output_format == "audio":
|
|
284
|
-
async for audio_chunk in self.llm_provider.tts(
|
|
285
|
-
text=processed_text,
|
|
286
|
-
voice=audio_voice,
|
|
287
|
-
response_format=audio_output_format
|
|
288
|
-
):
|
|
289
|
-
yield audio_chunk
|
|
290
|
-
else:
|
|
291
|
-
# For non-tool JSON, still capture the text
|
|
292
|
-
complete_text_response += json_buffer
|
|
293
|
-
|
|
294
|
-
if output_format == "audio":
|
|
295
|
-
async for audio_chunk in self.llm_provider.tts(
|
|
296
|
-
text=json_buffer,
|
|
297
|
-
voice=audio_voice,
|
|
298
|
-
response_format=audio_output_format
|
|
299
|
-
):
|
|
300
|
-
yield audio_chunk
|
|
301
|
-
else:
|
|
302
|
-
yield json_buffer
|
|
303
|
-
|
|
304
|
-
# Reset JSON handling
|
|
305
|
-
is_json = False
|
|
306
|
-
json_buffer = ""
|
|
307
|
-
|
|
308
|
-
except json.JSONDecodeError:
|
|
309
|
-
pass
|
|
297
|
+
full_prompt += f"ADDITIONAL PROMPT:\n{prompt}\n\n"
|
|
298
|
+
full_prompt += user_content
|
|
299
|
+
full_prompt += f"USER IDENTIFIER: {user_id}"
|
|
300
|
+
|
|
301
|
+
# Get OpenAI function schemas for this agent's tools
|
|
302
|
+
tools = [
|
|
303
|
+
{
|
|
304
|
+
"type": "function",
|
|
305
|
+
"function": {
|
|
306
|
+
"name": tool["name"],
|
|
307
|
+
"description": tool.get("description", ""),
|
|
308
|
+
"parameters": tool.get("parameters", {}),
|
|
309
|
+
"strict": True,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
for tool in self.get_agent_tools(agent_name)
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
# Structured output path
|
|
316
|
+
if output_model is not None:
|
|
317
|
+
model_instance = await self.llm_provider.parse_structured_output(
|
|
318
|
+
prompt=full_prompt,
|
|
319
|
+
system_prompt=system_prompt,
|
|
320
|
+
model_class=output_model,
|
|
321
|
+
api_key=self.api_key,
|
|
322
|
+
base_url=self.base_url,
|
|
323
|
+
model=self.model,
|
|
324
|
+
tools=tools if tools else None,
|
|
325
|
+
)
|
|
326
|
+
yield model_instance
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Vision fallback (non-streaming for now)
|
|
330
|
+
if images:
|
|
331
|
+
vision_text = await self.llm_provider.generate_text_with_images(
|
|
332
|
+
prompt=full_prompt, images=images, system_prompt=system_prompt
|
|
333
|
+
)
|
|
334
|
+
if output_format == "audio":
|
|
335
|
+
cleaned_audio_buffer = self._clean_for_audio(vision_text)
|
|
336
|
+
async for audio_chunk in self.llm_provider.tts(
|
|
337
|
+
text=cleaned_audio_buffer,
|
|
338
|
+
voice=audio_voice,
|
|
339
|
+
response_format=audio_output_format,
|
|
340
|
+
):
|
|
341
|
+
yield audio_chunk
|
|
310
342
|
else:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
343
|
+
yield vision_text
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Build initial messages for chat streaming
|
|
347
|
+
messages: List[Dict[str, Any]] = []
|
|
348
|
+
if system_prompt:
|
|
349
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
350
|
+
messages.append({"role": "user", "content": full_prompt})
|
|
351
|
+
|
|
352
|
+
accumulated_text = ""
|
|
353
|
+
|
|
354
|
+
# Loop to handle tool calls in streaming mode
|
|
355
|
+
while True:
|
|
356
|
+
# Aggregate tool calls by index and merge late IDs
|
|
357
|
+
tool_calls: Dict[int, Dict[str, Any]] = {}
|
|
358
|
+
|
|
359
|
+
async for event in self.llm_provider.chat_stream(
|
|
360
|
+
messages=messages,
|
|
361
|
+
model=self.model,
|
|
362
|
+
tools=tools if tools else None,
|
|
363
|
+
api_key=self.api_key,
|
|
364
|
+
base_url=self.base_url,
|
|
365
|
+
):
|
|
366
|
+
etype = event.get("type")
|
|
367
|
+
if etype == "content":
|
|
368
|
+
delta = event.get("delta", "")
|
|
369
|
+
accumulated_text += delta
|
|
370
|
+
if output_format == "text":
|
|
371
|
+
yield delta
|
|
372
|
+
elif etype == "tool_call_delta":
|
|
373
|
+
tc_id = event.get("id")
|
|
374
|
+
index_raw = event.get("index")
|
|
375
|
+
try:
|
|
376
|
+
index = int(index_raw) if index_raw is not None else 0
|
|
377
|
+
except Exception:
|
|
378
|
+
index = 0
|
|
379
|
+
name = event.get("name")
|
|
380
|
+
args_piece = event.get("arguments_delta", "")
|
|
381
|
+
entry = tool_calls.setdefault(
|
|
382
|
+
index, {"id": None, "name": None, "arguments": ""}
|
|
383
|
+
)
|
|
384
|
+
if tc_id and not entry.get("id"):
|
|
385
|
+
entry["id"] = tc_id
|
|
386
|
+
if name and not entry.get("name"):
|
|
387
|
+
entry["name"] = name
|
|
388
|
+
entry["arguments"] += args_piece
|
|
389
|
+
elif etype == "message_end":
|
|
390
|
+
_ = event.get("finish_reason")
|
|
391
|
+
|
|
392
|
+
# If tool calls were requested, execute them and continue the loop
|
|
393
|
+
if tool_calls:
|
|
394
|
+
assistant_tool_calls: List[Dict[str, Any]] = []
|
|
395
|
+
call_id_map: Dict[int, str] = {}
|
|
396
|
+
for idx, tc in tool_calls.items():
|
|
397
|
+
name = (tc.get("name") or "").strip()
|
|
398
|
+
if not name:
|
|
399
|
+
logger.warning(
|
|
400
|
+
f"Skipping unnamed tool call at index {idx}; cannot send empty function name."
|
|
401
|
+
)
|
|
402
|
+
continue
|
|
403
|
+
norm_id = tc.get("id") or f"call_{idx}"
|
|
404
|
+
call_id_map[idx] = norm_id
|
|
405
|
+
assistant_tool_calls.append(
|
|
406
|
+
{
|
|
407
|
+
"id": norm_id,
|
|
408
|
+
"type": "function",
|
|
409
|
+
"function": {
|
|
410
|
+
"name": name,
|
|
411
|
+
"arguments": tc.get("arguments") or "{}",
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if assistant_tool_calls:
|
|
417
|
+
messages.append(
|
|
418
|
+
{
|
|
419
|
+
"role": "assistant",
|
|
420
|
+
"content": None,
|
|
421
|
+
"tool_calls": assistant_tool_calls,
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Execute each tool and append the tool result messages
|
|
426
|
+
for idx, tc in tool_calls.items():
|
|
427
|
+
func_name = (tc.get("name") or "").strip()
|
|
428
|
+
if not func_name:
|
|
429
|
+
continue
|
|
430
|
+
try:
|
|
431
|
+
args = json.loads(tc.get("arguments") or "{}")
|
|
432
|
+
except Exception:
|
|
433
|
+
args = {}
|
|
434
|
+
logger.info(
|
|
435
|
+
f"Streaming: executing tool '{func_name}' with args: {args}"
|
|
436
|
+
)
|
|
437
|
+
tool_result = await self.execute_tool(
|
|
438
|
+
agent_name, func_name, args
|
|
439
|
+
)
|
|
440
|
+
messages.append(
|
|
441
|
+
{
|
|
442
|
+
"role": "tool",
|
|
443
|
+
"tool_call_id": call_id_map.get(idx, f"call_{idx}"),
|
|
444
|
+
"content": json.dumps(tool_result),
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
accumulated_text = ""
|
|
449
|
+
continue
|
|
338
450
|
|
|
451
|
+
# No tool calls: we've streamed the final answer
|
|
452
|
+
final_text = accumulated_text
|
|
339
453
|
if output_format == "audio":
|
|
454
|
+
cleaned_audio_buffer = self._clean_for_audio(final_text)
|
|
340
455
|
async for audio_chunk in self.llm_provider.tts(
|
|
341
|
-
text=
|
|
456
|
+
text=cleaned_audio_buffer,
|
|
342
457
|
voice=audio_voice,
|
|
343
|
-
response_format=audio_output_format
|
|
458
|
+
response_format=audio_output_format,
|
|
344
459
|
):
|
|
345
460
|
yield audio_chunk
|
|
346
461
|
else:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
self.last_text_response = complete_text_response
|
|
352
|
-
|
|
462
|
+
if not final_text:
|
|
463
|
+
yield ""
|
|
464
|
+
self.last_text_response = final_text
|
|
465
|
+
break
|
|
353
466
|
except Exception as e:
|
|
354
|
-
|
|
467
|
+
import traceback
|
|
468
|
+
|
|
469
|
+
error_msg = (
|
|
470
|
+
"I apologize, but I encountered an error processing your request."
|
|
471
|
+
)
|
|
472
|
+
logger.error(
|
|
473
|
+
f"Error in generate_response for agent '{agent_name}': {e}\n{traceback.format_exc()}"
|
|
474
|
+
)
|
|
355
475
|
if output_format == "audio":
|
|
356
|
-
async for chunk in self.llm_provider.tts(
|
|
476
|
+
async for chunk in self.llm_provider.tts(
|
|
477
|
+
error_msg,
|
|
478
|
+
voice=audio_voice,
|
|
479
|
+
response_format=audio_output_format,
|
|
480
|
+
):
|
|
357
481
|
yield chunk
|
|
358
482
|
else:
|
|
359
483
|
yield error_msg
|
|
360
484
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
print(traceback.format_exc())
|
|
485
|
+
def _clean_for_audio(self, text: str) -> str:
|
|
486
|
+
"""Remove Markdown formatting, emojis, and non-pronounceable characters from text."""
|
|
364
487
|
|
|
365
|
-
|
|
366
|
-
self,
|
|
367
|
-
agent_name: str,
|
|
368
|
-
json_chunk: str,
|
|
369
|
-
) -> str:
|
|
370
|
-
"""Handle tool calls and return formatted response."""
|
|
371
|
-
try:
|
|
372
|
-
data = json.loads(json_chunk)
|
|
373
|
-
if "tool_call" in data:
|
|
374
|
-
tool_data = data["tool_call"]
|
|
375
|
-
tool_name = tool_data.get("name")
|
|
376
|
-
parameters = tool_data.get("parameters", {})
|
|
377
|
-
|
|
378
|
-
if tool_name:
|
|
379
|
-
result = await self.execute_tool(
|
|
380
|
-
agent_name, tool_name, parameters)
|
|
381
|
-
if result.get("status") == "success":
|
|
382
|
-
return result.get("result", "")
|
|
383
|
-
else:
|
|
384
|
-
return f"I apologize, but I encountered an issue: {result.get('message', 'Unknown error')}"
|
|
385
|
-
return json_chunk
|
|
386
|
-
except json.JSONDecodeError:
|
|
387
|
-
return json_chunk
|
|
388
|
-
|
|
389
|
-
def _get_tool_usage_prompt(self, agent_name: str) -> str:
|
|
390
|
-
"""Generate JSON-based instructions for tool usage."""
|
|
391
|
-
# Get tools assigned to this agent
|
|
392
|
-
tools = self.get_agent_tools(agent_name)
|
|
393
|
-
if not tools:
|
|
488
|
+
if not text:
|
|
394
489
|
return ""
|
|
490
|
+
text = text.replace("’", "'").replace("‘", "'")
|
|
491
|
+
text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)
|
|
492
|
+
text = re.sub(r"`([^`]+)`", r"\1", text)
|
|
493
|
+
text = re.sub(r"(\*\*|__)(.*?)\1", r"\2", text)
|
|
494
|
+
text = re.sub(r"(\*|_)(.*?)\1", r"\2", text)
|
|
495
|
+
text = re.sub(r"^\s*#+\s*(.*?)$", r"\1", text, flags=re.MULTILINE)
|
|
496
|
+
text = re.sub(r"^\s*>\s*(.*?)$", r"\1", text, flags=re.MULTILINE)
|
|
497
|
+
text = re.sub(r"^\s*[-*_]{3,}\s*$", "", text, flags=re.MULTILINE)
|
|
498
|
+
text = re.sub(r"^\s*[-*+]\s+(.*?)$", r"\1", text, flags=re.MULTILINE)
|
|
499
|
+
text = re.sub(r"^\s*\d+\.\s+(.*?)$", r"\1", text, flags=re.MULTILINE)
|
|
500
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
501
|
+
emoji_pattern = re.compile(
|
|
502
|
+
"["
|
|
503
|
+
"\U0001f600-\U0001f64f" # emoticons
|
|
504
|
+
"\U0001f300-\U0001f5ff" # symbols & pictographs
|
|
505
|
+
"\U0001f680-\U0001f6ff" # transport & map symbols
|
|
506
|
+
"\U0001f700-\U0001f77f" # alchemical symbols
|
|
507
|
+
"\U0001f780-\U0001f7ff" # Geometric Shapes Extended
|
|
508
|
+
"\U0001f800-\U0001f8ff" # Supplemental Arrows-C
|
|
509
|
+
"\U0001f900-\U0001f9ff" # Supplemental Symbols and Pictographs
|
|
510
|
+
"\U0001fa70-\U0001faff" # Symbols and Pictographs Extended-A
|
|
511
|
+
"\U00002702-\U000027b0" # Dingbats
|
|
512
|
+
"\U000024c2-\U0001f251"
|
|
513
|
+
"\U00002600-\U000026ff" # Miscellaneous Symbols
|
|
514
|
+
"\U00002700-\U000027bf" # Dingbats
|
|
515
|
+
"\U0000fe00-\U0000fe0f" # Variation Selectors
|
|
516
|
+
"\U0001f1e0-\U0001f1ff" # Flags (iOS)
|
|
517
|
+
"]+",
|
|
518
|
+
flags=re.UNICODE,
|
|
519
|
+
)
|
|
520
|
+
text = emoji_pattern.sub(r" ", text)
|
|
521
|
+
text = re.sub(r"[^\w\s\.\,\;\:\?\!\'\"\-\(\)]", " ", text)
|
|
522
|
+
text = re.sub(r"\s+", " ", text)
|
|
523
|
+
return text.strip()
|
|
524
|
+
|
|
525
|
+
def _clean_tool_response(self, text: str) -> str:
|
|
526
|
+
"""Remove any tool markers or formatting that might have leaked into the response."""
|
|
527
|
+
if not text:
|
|
528
|
+
return ""
|
|
529
|
+
text = text.replace("[TOOL]", "").replace("[/TOOL]", "")
|
|
530
|
+
if text.lstrip().startswith("TOOL"):
|
|
531
|
+
text = text.lstrip()[4:].lstrip() # Remove "TOOL" and leading space
|
|
532
|
+
return text.strip()
|
|
533
|
+
|
|
534
|
+
# --- Add methods from factory logic ---
|
|
535
|
+
def load_and_register_plugins(self):
|
|
536
|
+
"""Loads plugins using the PluginManager."""
|
|
537
|
+
try:
|
|
538
|
+
self.plugin_manager.load_plugins()
|
|
539
|
+
logger.info("Plugins loaded successfully via PluginManager.")
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(f"Error loading plugins: {e}", exc_info=True)
|
|
395
542
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
543
|
+
def register_agents_from_config(self):
|
|
544
|
+
"""Registers agents defined in the main configuration."""
|
|
545
|
+
agents_config = self.config.get("agents", [])
|
|
546
|
+
if not agents_config:
|
|
547
|
+
logger.warning("No agents defined in the configuration.")
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
for agent_config in agents_config:
|
|
551
|
+
name = agent_config.get("name")
|
|
552
|
+
instructions = agent_config.get("instructions")
|
|
553
|
+
specialization = agent_config.get("specialization")
|
|
554
|
+
tools = agent_config.get("tools", [])
|
|
555
|
+
|
|
556
|
+
if not name or not instructions or not specialization:
|
|
557
|
+
logger.warning(
|
|
558
|
+
f"Skipping agent due to missing name, instructions, or specialization: {agent_config}"
|
|
559
|
+
)
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
self.register_ai_agent(name, instructions, specialization)
|
|
563
|
+
# logger.info(f"Registered agent: {name}") # Logging done in register_ai_agent
|
|
564
|
+
|
|
565
|
+
# Assign tools to the agent
|
|
566
|
+
for tool_name in tools:
|
|
567
|
+
if self.assign_tool_for_agent(name, tool_name):
|
|
568
|
+
logger.info(f"Assigned tool '{tool_name}' to agent '{name}'.")
|
|
569
|
+
else:
|
|
570
|
+
logger.warning(
|
|
571
|
+
f"Failed to assign tool '{tool_name}' to agent '{name}' (Tool might not be registered)."
|
|
572
|
+
)
|