solana-agent 24.0.0__tar.gz → 24.1.1__tar.gz

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 (35) hide show
  1. {solana_agent-24.0.0 → solana_agent-24.1.1}/PKG-INFO +3 -1
  2. {solana_agent-24.0.0 → solana_agent-24.1.1}/README.md +2 -0
  3. {solana_agent-24.0.0 → solana_agent-24.1.1}/pyproject.toml +1 -1
  4. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/adapters/llm_adapter.py +10 -10
  5. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/providers/llm.py +11 -1
  6. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/services/query.py +3 -0
  7. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/repositories/memory.py +2 -2
  8. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/services/agent.py +257 -152
  9. {solana_agent-24.0.0 → solana_agent-24.1.1}/LICENSE +0 -0
  10. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/__init__.py +0 -0
  11. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/adapters/__init__.py +0 -0
  12. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/adapters/mongodb_adapter.py +0 -0
  13. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/client/__init__.py +0 -0
  14. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/client/solana_agent.py +0 -0
  15. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/domains/__init__.py +0 -0
  16. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/domains/agent.py +0 -0
  17. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/domains/routing.py +0 -0
  18. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/factories/__init__.py +0 -0
  19. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/factories/agent_factory.py +0 -0
  20. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/__init__.py +0 -0
  21. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/client/client.py +0 -0
  22. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/plugins/plugins.py +0 -0
  23. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/providers/data_storage.py +0 -0
  24. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/providers/memory.py +0 -0
  25. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/services/agent.py +0 -0
  26. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/interfaces/services/routing.py +0 -0
  27. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/plugins/__init__.py +0 -0
  28. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/plugins/manager.py +0 -0
  29. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/plugins/registry.py +0 -0
  30. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/plugins/tools/__init__.py +0 -0
  31. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/plugins/tools/auto_tool.py +0 -0
  32. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/repositories/__init__.py +0 -0
  33. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/services/__init__.py +0 -0
  34. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/services/query.py +0 -0
  35. {solana_agent-24.0.0 → solana_agent-24.1.1}/solana_agent/services/routing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: solana-agent
3
- Version: 24.0.0
3
+ Version: 24.1.1
4
4
  Summary: Agentic IQ
5
5
  License: MIT
6
6
  Keywords: ai,openai,ai agents,agi
@@ -41,6 +41,7 @@ Build your AI business in three lines of code!
41
41
 
42
42
  ## Why?
43
43
  * Three lines of code setup
44
+ * Fast Responses
44
45
  * Multi-Agent Swarm
45
46
  * Multi-Modal Streaming (Text & Audio)
46
47
  * Conversational Memory & History
@@ -56,6 +57,7 @@ Build your AI business in three lines of code!
56
57
  ## Features
57
58
 
58
59
  * Easy three lines of code setup
60
+ * Fast AI responses
59
61
  * Designed for a multi-agent swarm
60
62
  * Seamless text and audio streaming with real-time multi-modal processing
61
63
  * Configurable audio voice characteristics via prompting
@@ -17,6 +17,7 @@ Build your AI business in three lines of code!
17
17
 
18
18
  ## Why?
19
19
  * Three lines of code setup
20
+ * Fast Responses
20
21
  * Multi-Agent Swarm
21
22
  * Multi-Modal Streaming (Text & Audio)
22
23
  * Conversational Memory & History
@@ -32,6 +33,7 @@ Build your AI business in three lines of code!
32
33
  ## Features
33
34
 
34
35
  * Easy three lines of code setup
36
+ * Fast AI responses
35
37
  * Designed for a multi-agent swarm
36
38
  * Seamless text and audio streaming with real-time multi-modal processing
37
39
  * Configurable audio voice characteristics via prompting
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "solana-agent"
3
- version = "24.0.0"
3
+ version = "24.1.1"
4
4
  description = "Agentic IQ"
5
5
  authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
6
6
  license = "MIT"
@@ -3,9 +3,9 @@ LLM provider adapters for the Solana Agent system.
3
3
 
4
4
  These adapters implement the LLMProvider interface for different LLM services.
5
5
  """
6
- from typing import AsyncGenerator, List, Literal, Type, TypeVar, Union
6
+ from typing import AsyncGenerator, Literal, Type, TypeVar
7
7
 
8
- from openai import OpenAI
8
+ from openai import AsyncOpenAI
9
9
  from pydantic import BaseModel
10
10
 
11
11
  from solana_agent.interfaces.providers.llm import LLMProvider
@@ -17,7 +17,7 @@ class OpenAIAdapter(LLMProvider):
17
17
  """OpenAI implementation of LLMProvider with web search capabilities."""
18
18
 
19
19
  def __init__(self, api_key: str):
20
- self.client = OpenAI(api_key=api_key)
20
+ self.client = AsyncOpenAI(api_key=api_key)
21
21
  self.parse_model = "gpt-4o-mini"
22
22
  self.text_model = "gpt-4o-mini"
23
23
  self.transcription_model = "gpt-4o-mini-transcribe"
@@ -44,7 +44,7 @@ class OpenAIAdapter(LLMProvider):
44
44
  Audio bytes as they become available
45
45
  """
46
46
  try:
47
- with self.client.audio.speech.with_streaming_response.create(
47
+ async with self.client.audio.speech.with_streaming_response.create(
48
48
  model=self.tts_model,
49
49
  voice=voice,
50
50
  instructions=instructions,
@@ -52,7 +52,7 @@ class OpenAIAdapter(LLMProvider):
52
52
  response_format=response_format
53
53
  ) as stream:
54
54
  # Stream the bytes in 16KB chunks
55
- for chunk in stream.iter_bytes(chunk_size=1024 * 16):
55
+ async for chunk in stream.iter_bytes(chunk_size=1024 * 16):
56
56
  yield chunk
57
57
 
58
58
  except Exception as e:
@@ -84,13 +84,13 @@ class OpenAIAdapter(LLMProvider):
84
84
  Transcript text chunks as they become available
85
85
  """
86
86
  try:
87
- with self.client.audio.transcriptions.with_streaming_response.create(
87
+ async with self.client.audio.transcriptions.with_streaming_response.create(
88
88
  model=self.transcription_model,
89
89
  file=(f"file.{input_format}", audio_bytes),
90
90
  response_format="text",
91
91
  ) as stream:
92
92
  # Stream the text in 16KB chunks
93
- for chunk in stream.iter_text(chunk_size=1024 * 16):
93
+ async for chunk in stream.iter_text(chunk_size=1024 * 16):
94
94
  yield chunk
95
95
 
96
96
  except Exception as e:
@@ -119,9 +119,9 @@ class OpenAIAdapter(LLMProvider):
119
119
  "model": self.text_model,
120
120
  }
121
121
  try:
122
- response = self.client.chat.completions.create(**request_params)
122
+ response = await self.client.chat.completions.create(**request_params)
123
123
 
124
- for chunk in response:
124
+ async for chunk in response:
125
125
  if chunk.choices:
126
126
  if chunk.choices[0].delta.content:
127
127
  text = chunk.choices[0].delta.content
@@ -148,7 +148,7 @@ class OpenAIAdapter(LLMProvider):
148
148
 
149
149
  try:
150
150
  # First try the beta parsing API
151
- completion = self.client.beta.chat.completions.parse(
151
+ completion = await self.client.beta.chat.completions.parse(
152
152
  model=self.parse_model,
153
153
  messages=messages,
154
154
  response_format=model_class,
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import AsyncGenerator, List, Literal, Type, TypeVar, Union
2
+ from typing import Any, AsyncGenerator, Callable, Dict, List, Literal, Optional, Type, TypeVar, Union
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
@@ -49,3 +49,13 @@ class LLMProvider(ABC):
49
49
  ) -> AsyncGenerator[str, None]:
50
50
  """Transcribe audio from the language model."""
51
51
  pass
52
+
53
+ @abstractmethod
54
+ async def realtime_audio_transcription(
55
+ self,
56
+ audio_generator: AsyncGenerator[bytes, None],
57
+ transcription_config: Optional[Dict[str, Any]] = None,
58
+ on_event: Optional[Callable[[Dict[str, Any]], Any]] = None,
59
+ ) -> AsyncGenerator[str, None]:
60
+ """Stream real-time audio transcription from the language model."""
61
+ pass
@@ -1,6 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Any, AsyncGenerator, Dict, Literal, Optional, Union
3
3
 
4
+ from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
5
+
4
6
 
5
7
  class QueryService(ABC):
6
8
  """Interface for processing user queries."""
@@ -20,6 +22,7 @@ class QueryService(ABC):
20
22
  "flac", "mp3", "mp4", "mpeg", "mpga", "m4a", "ogg", "wav", "webm"
21
23
  ] = "mp4",
22
24
  prompt: Optional[str] = None,
25
+ router: Optional[RoutingInterface] = None,
23
26
  ) -> AsyncGenerator[Union[str, bytes], None]:
24
27
  """Process the user request and generate a response."""
25
28
  pass
@@ -69,8 +69,8 @@ class MemoryRepository(MemoryProvider):
69
69
  # Store truncated messages
70
70
  doc = {
71
71
  "user_id": user_id,
72
- "user_message": self._truncate(user_msg),
73
- "assistant_message": self._truncate(assistant_msg),
72
+ "user_message": user_msg,
73
+ "assistant_message": assistant_msg,
74
74
  "timestamp": datetime.now(timezone.utc)
75
75
  }
76
76
  self.mongo.insert_one(self.collection, doc)
@@ -191,7 +191,7 @@ class AgentService(AgentServiceInterface):
191
191
  return
192
192
 
193
193
  try:
194
- # Handle audio input if provided
194
+ # Handle audio input if provided - KEEP REAL-TIME AUDIO TRANSCRIPTION
195
195
  query_text = ""
196
196
  if not isinstance(query, str):
197
197
  async for transcript in self.llm_provider.transcribe_audio(query, input_format=audio_input_format):
@@ -209,118 +209,172 @@ class AgentService(AgentServiceInterface):
209
209
  if prompt:
210
210
  system_prompt += f"\n\nADDITIONAL PROMPT: {prompt}"
211
211
 
212
- # make tool calling prompt
212
+ # Add tool usage prompt if tools are available
213
213
  tool_calling_system_prompt = deepcopy(system_prompt)
214
214
  if self.tool_registry:
215
215
  tool_usage_prompt = self._get_tool_usage_prompt(agent_name)
216
216
  if tool_usage_prompt:
217
217
  tool_calling_system_prompt += f"\n\nTOOL CALLING PROMPT: {tool_usage_prompt}"
218
+ print(
219
+ f"Tools available to agent {agent_name}: {[t.get('name') for t in self.get_agent_tools(agent_name)]}")
218
220
 
219
- # Variables for tracking the response
221
+ # Variables for tracking the complete response
220
222
  complete_text_response = ""
221
-
222
- # For audio output, we'll collect everything first
223
223
  full_response_buffer = ""
224
224
 
225
- # Variables for handling JSON processing
226
- json_buffer = ""
227
- is_json = False
225
+ # Variables for robust handling of tool call markers that may be split across chunks
226
+ tool_buffer = ""
227
+ pending_chunk = "" # To hold text that might contain partial markers
228
+ is_tool_call = False
229
+ window_size = 30 # Increased window size for better detection
230
+
231
+ # Define start and end markers
232
+ start_marker = "[TOOL]"
233
+ end_marker = "[/TOOL]"
228
234
 
229
- # Generate and stream response
235
+ # Generate and stream response (ALWAYS use non-realtime for text generation)
236
+ print(
237
+ f"Generating response with {len(query_text)} characters of query text")
230
238
  async for chunk in self.llm_provider.generate_text(
231
239
  prompt=query_text,
232
240
  system_prompt=tool_calling_system_prompt,
233
241
  ):
234
- # Check if the chunk is JSON or a tool call
235
- if (chunk.strip().startswith("{") or "{\"tool_call\":" in chunk) and not is_json:
236
- is_json = True
237
- json_buffer = chunk
242
+ # If we have pending text from the previous chunk, combine it with this chunk
243
+ if pending_chunk:
244
+ combined_chunk = pending_chunk + chunk
245
+ pending_chunk = "" # Reset pending chunk
246
+ else:
247
+ combined_chunk = chunk
248
+
249
+ # STEP 1: Check for tool call start marker
250
+ if start_marker in combined_chunk and not is_tool_call:
251
+ print(
252
+ f"Found tool start marker in chunk of length {len(combined_chunk)}")
253
+ is_tool_call = True
254
+
255
+ # Extract text before the marker and the marker itself with everything after
256
+ start_pos = combined_chunk.find(start_marker)
257
+ before_marker = combined_chunk[:start_pos]
258
+ after_marker = combined_chunk[start_pos:]
259
+
260
+ # Yield text that appeared before the marker
261
+ if before_marker and output_format == "text":
262
+ yield before_marker
263
+
264
+ # Start collecting the tool call
265
+ tool_buffer = after_marker
266
+ continue # Skip to next chunk
267
+
268
+ # STEP 2: Handle ongoing tool call collection
269
+ if is_tool_call:
270
+ tool_buffer += combined_chunk
271
+
272
+ # Check if the tool call is complete
273
+ if end_marker in tool_buffer:
274
+ print(
275
+ f"Tool call complete, buffer size: {len(tool_buffer)}")
276
+
277
+ # Process the tool call
278
+ response_text = await self._handle_tool_call(
279
+ agent_name=agent_name,
280
+ tool_text=tool_buffer
281
+ )
282
+
283
+ # Clean the response to remove any markers or formatting
284
+ response_text = self._clean_tool_response(
285
+ response_text)
286
+ print(
287
+ f"Tool execution complete, result size: {len(response_text)}")
288
+
289
+ # Create new prompt with search/tool results
290
+ # Using "Search Result" instead of "TOOL RESPONSE" to avoid model repeating "TOOL"
291
+ user_prompt = f"{query_text}\n\nSearch Result: {response_text}"
292
+ tool_system_prompt = system_prompt + \
293
+ "\n DO NOT use the tool calling format again."
294
+
295
+ # Generate a new response with the tool results
296
+ print("Generating new response with tool results")
297
+ if output_format == "text":
298
+ # Stream the follow-up response for text output
299
+ async for processed_chunk in self.llm_provider.generate_text(
300
+ prompt=user_prompt,
301
+ system_prompt=tool_system_prompt,
302
+ ):
303
+ complete_text_response += processed_chunk
304
+ yield processed_chunk
305
+ else:
306
+ # For audio output, collect the full response first
307
+ tool_response = ""
308
+ async for processed_chunk in self.llm_provider.generate_text(
309
+ prompt=user_prompt,
310
+ system_prompt=tool_system_prompt,
311
+ ):
312
+ tool_response += processed_chunk
313
+
314
+ # Clean and add to our complete text record and audio buffer
315
+ tool_response = self._clean_for_audio(
316
+ tool_response)
317
+ complete_text_response += tool_response
318
+ full_response_buffer += tool_response
319
+
320
+ # Reset tool handling state
321
+ is_tool_call = False
322
+ tool_buffer = ""
323
+ pending_chunk = ""
324
+ break # Exit the original generation loop after tool processing
325
+
326
+ # Continue collecting tool call content without yielding
238
327
  continue
239
328
 
240
- # Collect JSON or handle normal text
241
- if is_json:
242
- json_buffer += chunk
243
- try:
244
- # Try to parse complete JSON
245
- data = json.loads(json_buffer)
246
-
247
- # Valid JSON found, handle it
248
- if "tool_call" in data:
249
- response_text = await self._handle_tool_call(
250
- agent_name=agent_name,
251
- json_chunk=json_buffer
252
- )
253
-
254
- # Update system prompt to prevent further tool calls
255
- tool_system_prompt = system_prompt + \
256
- "\n DO NOT make any tool calls or return JSON."
257
-
258
- # Create prompt with tool response
259
- user_prompt = f"\n USER QUERY: {query_text} \n"
260
- user_prompt += f"\n TOOL RESPONSE: {response_text} \n"
261
-
262
- # For text output, process chunks directly
263
- if output_format == "text":
264
- # Stream text response for text output
265
- async for processed_chunk in self.llm_provider.generate_text(
266
- prompt=user_prompt,
267
- system_prompt=tool_system_prompt,
268
- ):
269
- complete_text_response += processed_chunk
270
- yield processed_chunk
271
- else:
272
- # For audio output, collect the full tool response first
273
- tool_response = ""
274
- async for processed_chunk in self.llm_provider.generate_text(
275
- prompt=user_prompt,
276
- system_prompt=tool_system_prompt,
277
- ):
278
- tool_response += processed_chunk
279
-
280
- # Add to our complete text record and full audio buffer
281
- tool_response = self._clean_for_audio(
282
- tool_response)
283
- complete_text_response += tool_response
284
- full_response_buffer += tool_response
285
- else:
286
- # For non-tool JSON, still capture the text
287
- complete_text_response += json_buffer
288
-
289
- if output_format == "text":
290
- yield json_buffer
291
- else:
292
- # Add to full response buffer for audio
293
- full_response_buffer += json_buffer
294
-
295
- # Reset JSON handling
296
- is_json = False
297
- json_buffer = ""
298
-
299
- except json.JSONDecodeError:
300
- # JSON not complete yet, continue collecting
301
- pass
302
- else:
303
- # For regular text
304
- complete_text_response += chunk
305
-
306
- if output_format == "text":
307
- # For text output, yield directly
308
- yield chunk
309
- else:
310
- # For audio output, add to the full response buffer
311
- full_response_buffer += chunk
312
-
313
- # Handle any leftover JSON buffer
314
- if json_buffer:
315
- complete_text_response += json_buffer
329
+ # STEP 3: Check for possible partial start markers at the end of the chunk
330
+ # This helps detect markers split across chunks
331
+ potential_marker = False
332
+ for i in range(1, len(start_marker)):
333
+ if combined_chunk.endswith(start_marker[:i]):
334
+ # Found a partial marker at the end
335
+ # Save the partial marker
336
+ pending_chunk = combined_chunk[-i:]
337
+ # Everything except the partial marker
338
+ chunk_to_yield = combined_chunk[:-i]
339
+ potential_marker = True
340
+ print(
341
+ f"Potential partial marker detected: '{pending_chunk}'")
342
+ break
343
+
344
+ if potential_marker:
345
+ # Process the safe part of the chunk
346
+ if chunk_to_yield and output_format == "text":
347
+ yield chunk_to_yield
348
+ if chunk_to_yield:
349
+ complete_text_response += chunk_to_yield
350
+ if output_format == "audio":
351
+ full_response_buffer += chunk_to_yield
352
+ continue
353
+
354
+ # STEP 4: Normal text processing for non-tool call content
316
355
  if output_format == "text":
317
- yield json_buffer
318
- else:
319
- full_response_buffer += json_buffer
356
+ yield combined_chunk
320
357
 
321
- # For audio output, now process the complete response
358
+ complete_text_response += combined_chunk
359
+ if output_format == "audio":
360
+ full_response_buffer += combined_chunk
361
+
362
+ # Process any incomplete tool call as regular text
363
+ if is_tool_call and tool_buffer:
364
+ print(
365
+ f"Incomplete tool call detected, returning as regular text: {len(tool_buffer)} chars")
366
+ if output_format == "text":
367
+ yield tool_buffer
368
+
369
+ complete_text_response += tool_buffer
370
+ if output_format == "audio":
371
+ full_response_buffer += tool_buffer
372
+
373
+ # For audio output, generate speech from the complete buffer
322
374
  if output_format == "audio" and full_response_buffer:
323
375
  # Clean text before TTS
376
+ print(
377
+ f"Processing {len(full_response_buffer)} characters for audio output")
324
378
  full_response_buffer = self._clean_for_audio(
325
379
  full_response_buffer)
326
380
 
@@ -335,9 +389,15 @@ class AgentService(AgentServiceInterface):
335
389
 
336
390
  # Store the complete text response
337
391
  self.last_text_response = complete_text_response
392
+ print(
393
+ f"Response generation complete: {len(complete_text_response)} chars")
338
394
 
339
395
  except Exception as e:
340
396
  error_msg = f"I apologize, but I encountered an error: {str(e)}"
397
+ print(f"Error in generate_response: {str(e)}")
398
+ import traceback
399
+ print(traceback.format_exc())
400
+
341
401
  if output_format == "audio":
342
402
  async for chunk in self.llm_provider.tts(
343
403
  error_msg,
@@ -349,52 +409,73 @@ class AgentService(AgentServiceInterface):
349
409
  else:
350
410
  yield error_msg
351
411
 
352
- print(f"Error in generate_response: {str(e)}")
353
- import traceback
354
- print(traceback.format_exc())
412
+ async def _bytes_to_generator(self, data: bytes) -> AsyncGenerator[bytes, None]:
413
+ """Convert bytes to an async generator for streaming.
355
414
 
356
- async def _handle_tool_call(
357
- self,
358
- agent_name: str,
359
- json_chunk: str,
360
- ) -> str:
361
- """Handle tool calls and return formatted response."""
415
+ Args:
416
+ data: Bytes of audio data
417
+
418
+ Yields:
419
+ Chunks of audio data
420
+ """
421
+ # Define a reasonable chunk size (adjust based on your needs)
422
+ chunk_size = 4096
423
+
424
+ for i in range(0, len(data), chunk_size):
425
+ yield data[i:i + chunk_size]
426
+ # Small delay to simulate streaming
427
+ await asyncio.sleep(0.01)
428
+
429
+ async def _handle_tool_call(self, agent_name: str, tool_text: str) -> str:
430
+ """Handle marker-based tool calls."""
362
431
  try:
363
- data = json.loads(json_chunk)
364
- if "tool_call" in data:
365
- tool_data = data["tool_call"]
366
- tool_name = tool_data.get("name")
367
- parameters = tool_data.get("parameters", {})
368
-
369
- if tool_name:
370
- # Execute the tool and get the result
371
- result = await self.execute_tool(agent_name, tool_name, parameters)
372
-
373
- if result.get("status") == "success":
374
- tool_result = result.get("result", "")
375
- return tool_result
376
- else:
377
- error_message = f"I apologize, but I encountered an issue with the {tool_name} tool: {result.get('message', 'Unknown error')}"
378
- print(f"Tool error: {error_message}")
379
- return error_message
380
- else:
381
- return "Tool name was not provided in the tool call."
432
+ # Extract the content between markers
433
+ start_marker = "[TOOL]"
434
+ end_marker = "[/TOOL]"
435
+
436
+ start_idx = tool_text.find(start_marker) + len(start_marker)
437
+ end_idx = tool_text.find(end_marker)
438
+
439
+ tool_content = tool_text[start_idx:end_idx].strip()
440
+
441
+ # Parse the lines to extract name and parameters
442
+ tool_name = None
443
+ parameters = {}
444
+
445
+ for line in tool_content.split("\n"):
446
+ line = line.strip()
447
+ if not line:
448
+ continue
449
+
450
+ if line.startswith("name:"):
451
+ tool_name = line[5:].strip()
452
+ elif line.startswith("parameters:"):
453
+ params_text = line[11:].strip()
454
+ # Parse comma-separated parameters
455
+ param_pairs = params_text.split(",")
456
+ for pair in param_pairs:
457
+ if "=" in pair:
458
+ k, v = pair.split("=", 1)
459
+ parameters[k.strip()] = v.strip()
460
+
461
+ # Execute the tool
462
+ result = await self.execute_tool(agent_name, tool_name, parameters)
463
+
464
+ # Return the result as string
465
+ if result.get("status") == "success":
466
+ tool_result = str(result.get("result", ""))
467
+ return tool_result
382
468
  else:
383
- print(f"JSON received but no tool_call found: {json_chunk}")
469
+ error_msg = f"Error calling {tool_name}: {result.get('message', 'Unknown error')}"
470
+ return error_msg
384
471
 
385
- # If we get here, it wasn't properly handled as a tool
386
- return f"The following request was not processed as a valid tool call:\n{json_chunk}"
387
- except json.JSONDecodeError as e:
388
- print(f"JSON decode error in tool call: {e}")
389
- return json_chunk
390
472
  except Exception as e:
391
- print(f"Unexpected error in tool call handling: {str(e)}")
392
473
  import traceback
393
474
  print(traceback.format_exc())
394
475
  return f"Error processing tool call: {str(e)}"
395
476
 
396
477
  def _get_tool_usage_prompt(self, agent_name: str) -> str:
397
- """Generate JSON-based instructions for tool usage."""
478
+ """Generate marker-based instructions for tool usage."""
398
479
  # Get tools assigned to this agent
399
480
  tools = self.get_agent_tools(agent_name)
400
481
  if not tools:
@@ -405,29 +486,38 @@ class AgentService(AgentServiceInterface):
405
486
  tools_json = json.dumps(tools, indent=2)
406
487
 
407
488
  return f"""
408
- AVAILABLE TOOLS:
409
- {tools_json}
410
-
411
- TOOL USAGE FORMAT:
412
- {{
413
- "tool_call": {{
414
- "name": "<one_of:{', '.join(available_tool_names)}>",
415
- "parameters": {{
416
- // parameters as specified in tool definition above
417
- }}
418
- }}
419
- }}
420
-
421
- RESPONSE RULES:
422
- 1. For tool usage:
423
- - Only use tools from the AVAILABLE TOOLS list above
424
- - Follow the exact parameter format shown in the tool definition
425
-
426
- 2. Format Requirements:
427
- - Return ONLY the JSON object for tool calls
428
- - No explanation text before or after
429
- - Use exact tool names as shown in AVAILABLE TOOLS
430
- """
489
+ AVAILABLE TOOLS:
490
+ {tools_json}
491
+
492
+ ⚠️ CRITICAL INSTRUCTION: When using a tool, NEVER include explanatory text.
493
+ Only output the exact tool call format shown below with NO other text.
494
+
495
+ TOOL USAGE FORMAT:
496
+ [TOOL]
497
+ name: tool_name
498
+ parameters: key1=value1, key2=value2
499
+ [/TOOL]
500
+
501
+ EXAMPLES:
502
+
503
+ CORRECT - ONLY the tool call with NOTHING else:
504
+ [TOOL]
505
+ name: search_internet
506
+ parameters: query=latest news on Solana
507
+ [/TOOL]
508
+
509
+ ❌ INCORRECT - Never add explanatory text like this:
510
+ To get the latest news on Solana, I will search the internet.
511
+ [TOOL]
512
+ name: search_internet
513
+ parameters: query=latest news on Solana
514
+ [/TOOL]
515
+
516
+ REMEMBER:
517
+ 1. Output ONLY the exact tool call format with NO additional text
518
+ 2. After seeing your tool call, I will execute it automatically
519
+ 3. You will receive the tool results and can then respond to the user
520
+ """
431
521
 
432
522
  def _clean_for_audio(self, text: str) -> str:
433
523
  """Remove Markdown formatting, emojis, and non-pronounceable characters from text.
@@ -502,3 +592,18 @@ class AgentService(AgentServiceInterface):
502
592
  text = re.sub(r'\s+', ' ', text)
503
593
 
504
594
  return text.strip()
595
+
596
+ def _clean_tool_response(self, text: str) -> str:
597
+ """Remove any tool markers or formatting that might have leaked into the response."""
598
+ if not text:
599
+ return ""
600
+
601
+ # Remove any tool markers that might be in the response
602
+ text = text.replace("[TOOL]", "")
603
+ text = text.replace("[/TOOL]", "")
604
+
605
+ # Remove the word TOOL from start if it appears
606
+ if text.lstrip().startswith("TOOL"):
607
+ text = text.lstrip().replace("TOOL", "", 1)
608
+
609
+ return text.strip()
File without changes