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.
Files changed (45) hide show
  1. solana_agent/__init__.py +10 -5
  2. solana_agent/adapters/ffmpeg_transcoder.py +375 -0
  3. solana_agent/adapters/mongodb_adapter.py +15 -2
  4. solana_agent/adapters/openai_adapter.py +679 -0
  5. solana_agent/adapters/openai_realtime_ws.py +1813 -0
  6. solana_agent/adapters/pinecone_adapter.py +543 -0
  7. solana_agent/cli.py +128 -0
  8. solana_agent/client/solana_agent.py +180 -20
  9. solana_agent/domains/agent.py +13 -13
  10. solana_agent/domains/routing.py +18 -8
  11. solana_agent/factories/agent_factory.py +239 -38
  12. solana_agent/guardrails/pii.py +107 -0
  13. solana_agent/interfaces/client/client.py +95 -12
  14. solana_agent/interfaces/guardrails/guardrails.py +26 -0
  15. solana_agent/interfaces/plugins/plugins.py +2 -1
  16. solana_agent/interfaces/providers/__init__.py +0 -0
  17. solana_agent/interfaces/providers/audio.py +40 -0
  18. solana_agent/interfaces/providers/data_storage.py +9 -2
  19. solana_agent/interfaces/providers/llm.py +86 -9
  20. solana_agent/interfaces/providers/memory.py +13 -1
  21. solana_agent/interfaces/providers/realtime.py +212 -0
  22. solana_agent/interfaces/providers/vector_storage.py +53 -0
  23. solana_agent/interfaces/services/agent.py +27 -12
  24. solana_agent/interfaces/services/knowledge_base.py +59 -0
  25. solana_agent/interfaces/services/query.py +41 -8
  26. solana_agent/interfaces/services/routing.py +0 -1
  27. solana_agent/plugins/manager.py +37 -16
  28. solana_agent/plugins/registry.py +34 -19
  29. solana_agent/plugins/tools/__init__.py +0 -5
  30. solana_agent/plugins/tools/auto_tool.py +1 -0
  31. solana_agent/repositories/memory.py +332 -111
  32. solana_agent/services/__init__.py +1 -1
  33. solana_agent/services/agent.py +390 -241
  34. solana_agent/services/knowledge_base.py +768 -0
  35. solana_agent/services/query.py +1858 -153
  36. solana_agent/services/realtime.py +626 -0
  37. solana_agent/services/routing.py +104 -51
  38. solana_agent-31.4.0.dist-info/METADATA +1070 -0
  39. solana_agent-31.4.0.dist-info/RECORD +49 -0
  40. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
  41. solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
  42. solana_agent/adapters/llm_adapter.py +0 -160
  43. solana_agent-20.1.2.dist-info/METADATA +0 -464
  44. solana_agent-20.1.2.dist-info/RECORD +0 -35
  45. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
@@ -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
- import asyncio
7
+
8
8
  import datetime as main_datetime
9
9
  from datetime import datetime
10
10
  import json
11
- from typing import AsyncGenerator, Dict, List, Literal, Optional, Any, Union
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, name: str, instructions: str, specialization: str,
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
- f"- {value.get('name', '')}: {value.get('description', '')}"
93
- for value in self.business_mission.values
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(self, agent_name: str, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
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
- print(traceback.format_exc())
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["alloy", "ash", "ballad", "coral", "echo",
171
- "fable", "onyx", "nova", "sage", "shimmer"] = "nova",
172
- audio_instructions: Optional[str] = None,
173
- audio_output_format: Literal['mp3', 'opus',
174
- 'aac', 'flac', 'wav', 'pcm'] = "aac",
175
- audio_input_format: Literal[
176
- "flac", "mp3", "mp4", "mpeg", "mpga", "m4a", "ogg", "wav", "webm"
177
- ] = "mp4",
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
- ) -> AsyncGenerator[Union[str, bytes], None]: # pragma: no cover
180
- """Generate a response with support for text/audio input/output.
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
- # Handle audio input if provided
209
- query_text = ""
210
- if not isinstance(query, str):
211
- async for transcript in self.llm_provider.transcribe_audio(query, input_format=audio_input_format):
212
- query_text += transcript
213
- else:
214
- query_text = query
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
- # Get system prompt and add tool instructions
286
+ # Build system prompt and messages
217
287
  system_prompt = self.get_agent_system_prompt(agent_name)
218
- if self.tool_registry:
219
- tool_usage_prompt = self._get_tool_usage_prompt(agent_name)
220
- if tool_usage_prompt:
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
- # Add User ID and memory context
224
- system_prompt += f"\n\nUser ID: {user_id}"
292
+ # Compose the prompt for generate_text
293
+ full_prompt = ""
225
294
  if memory_context:
226
- system_prompt += f"\n\nMEMORY CONTEXT: {memory_context}"
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
- system_prompt += f"\n\nADDITIONAL PROMPT: {prompt}"
229
-
230
- # Keep track of the complete text response
231
- complete_text_response = ""
232
- json_buffer = ""
233
- is_json = False
234
- text_buffer = ""
235
-
236
- # Generate and stream response
237
- async for chunk in self.llm_provider.generate_text(
238
- prompt=query_text,
239
- system_prompt=system_prompt,
240
- ):
241
- # Check for JSON start
242
- if chunk.strip().startswith("{") and not is_json:
243
- is_json = True
244
- json_buffer = chunk
245
- continue
246
-
247
- # Collect JSON or handle normal text
248
- if is_json:
249
- json_buffer += chunk
250
- try:
251
- # Try to parse complete JSON
252
- data = json.loads(json_buffer)
253
-
254
- # Valid JSON found, handle it
255
- if "tool_call" in data:
256
- # Process tool call with existing method
257
- response_text = await self._handle_tool_call(
258
- agent_name=agent_name,
259
- json_chunk=json_buffer
260
- )
261
-
262
- system_prompt = system_prompt + \
263
- "\n DO NOT make any tool calls or return JSON."
264
-
265
- user_prompt = f"\n USER QUERY: {query_text} \n"
266
- user_prompt += f"\n TOOL RESPONSE: {response_text} \n"
267
-
268
- # Collect all processed text first
269
- processed_text = ""
270
- async for processed_chunk in self.llm_provider.generate_text(
271
- prompt=user_prompt,
272
- system_prompt=system_prompt,
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
- # For regular text, always add to the complete response
312
- complete_text_response += chunk
313
-
314
- # Handle audio buffering or direct text output
315
- if output_format == "audio":
316
- text_buffer += chunk
317
- if any(punct in chunk for punct in ".!?"):
318
- async for audio_chunk in self.llm_provider.tts(
319
- text=text_buffer,
320
- voice=audio_voice,
321
- response_format=audio_output_format
322
- ):
323
- yield audio_chunk
324
- text_buffer = ""
325
- else:
326
- yield chunk
327
-
328
- # Handle any remaining text or incomplete JSON
329
- remaining_text = ""
330
- if text_buffer:
331
- remaining_text += text_buffer
332
- if is_json and json_buffer:
333
- remaining_text += json_buffer
334
-
335
- if remaining_text:
336
- # Add remaining text to complete response
337
- complete_text_response += remaining_text
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=remaining_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
- yield remaining_text
348
-
349
- # Store the complete text response for the caller to access
350
- # This needs to be done in the query service using the self.last_text_response
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
- error_msg = f"I apologize, but I encountered an error: {str(e)}"
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(error_msg, voice=audio_voice, response_format=audio_output_format):
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
- print(f"Error in generate_response: {str(e)}")
362
- import traceback
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
- async def _handle_tool_call(
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
- # Get actual tool names
397
- available_tool_names = [tool.get("name", "") for tool in tools]
398
- tools_json = json.dumps(tools, indent=2)
399
-
400
- return f"""
401
- AVAILABLE TOOLS:
402
- {tools_json}
403
-
404
- TOOL USAGE FORMAT:
405
- {{
406
- "tool_call": {{
407
- "name": "<one_of:{', '.join(available_tool_names)}>",
408
- "parameters": {{
409
- // parameters as specified in tool definition above
410
- }}
411
- }}
412
- }}
413
-
414
- RESPONSE RULES:
415
- 1. For tool usage:
416
- - Only use tools from the AVAILABLE TOOLS list above
417
- - Follow the exact parameter format shown in the tool definition
418
-
419
- 2. Format Requirements:
420
- - Return ONLY the JSON object for tool calls
421
- - No explanation text before or after
422
- - Use exact tool names as shown in AVAILABLE TOOLS
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
+ )