native tool call wip

This commit is contained in:
marko-kraemer 2025-04-07 18:35:40 +01:00
parent 91081bc646
commit 6eb516e61d
8 changed files with 499 additions and 290 deletions

View File

@ -22,7 +22,6 @@ Remember:
5. Focus on providing accurate, helpful information
6. Consider context and user needs in your responses
7. Handle ambiguity gracefully by asking clarifying questions when needed
8. ISSUE ONLY ONE SINGLE XML TOOL CALL AT A TIME - complete one action before proceeding to the next
<available_tools>
You have access to these tools through XML-based tool calling:
@ -38,12 +37,12 @@ You have access to these tools through XML-based tool calling:
"""
#Wait for each action to complete before proceeding to the next one.
RESPONSE_FORMAT = """
<response_format>
RESPONSE FORMAT STRICTLY Output XML tags for tool calling
You must only use ONE tool call at a time. Wait for each action to complete before proceeding to the next one.
<create-file file_path="path/to/file">
file contents here
</create-file>
@ -82,5 +81,5 @@ def get_system_prompt():
'''
Returns the system prompt with XML tool usage instructions.
'''
return SYSTEM_PROMPT
return SYSTEM_PROMPT
#+ RESPONSE_FORMAT

View File

@ -4,7 +4,6 @@ import uuid
from agentpress.thread_manager import ThreadManager
from agent.tools.files_tool import FilesTool
from agent.tools.terminal_tool import TerminalTool
from agent.tools.wait_tool import WaitTool
# from agent.tools.search_tool import CodeSearchTool
from typing import Optional
from agent.prompt import get_system_prompt
@ -23,7 +22,6 @@ async def run_agent(thread_id: str, stream: bool = True, thread_manager: Optiona
print("Adding tools to thread manager...")
thread_manager.add_tool(FilesTool)
thread_manager.add_tool(TerminalTool)
thread_manager.add_tool(WaitTool)
# thread_manager.add_tool(CodeSearchTool)
system_message = {
@ -31,7 +29,12 @@ async def run_agent(thread_id: str, stream: bool = True, thread_manager: Optiona
"content": get_system_prompt()
}
model_name = "bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0" #groq/deepseek-r1-distill-llama-70b
model_name = "anthropic/claude-3-5-sonnet-latest"
#anthropic/claude-3-7-sonnet-latest
#openai/gpt-4o
#groq/deepseek-r1-distill-llama-70b
#bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0
files_tool = FilesTool()
@ -55,12 +58,13 @@ Current development environment workspace state:
llm_model=model_name,
llm_temperature=0.1,
llm_max_tokens=8000,
tool_choice="any",
processor_config=ProcessorConfig(
xml_tool_calling=False,
native_tool_calling=True,
execute_tools=True,
execute_on_stream=True,
tool_execution_strategy="sequential",
tool_execution_strategy="parallel",
xml_adding_strategy="user_message"
)
)

View File

@ -1,176 +0,0 @@
"""
Wait Tool for testing sequential vs parallel tool execution.
This tool provides methods with configurable delays to test and demonstrate
the different tool execution strategies in AgentPress.
"""
import asyncio
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
from utils.logger import logger
class WaitTool(Tool):
"""Tool that introduces configurable delays.
This tool is useful for testing and demonstrating sequential vs parallel
tool execution strategies by creating observable delays.
"""
def __init__(self):
"""Initialize the WaitTool."""
super().__init__()
logger.info("Initialized WaitTool for testing execution strategies")
@xml_schema(
tag_name="wait",
mappings=[
{"param_name": "seconds", "node_type": "attribute", "path": "."},
{"param_name": "message", "node_type": "content", "path": "."}
],
example='''
<wait seconds="3">This will wait for 3 seconds</wait>
'''
)
@openapi_schema({
"type": "function",
"function": {
"name": "wait",
"description": "Wait for a specified number of seconds",
"parameters": {
"type": "object",
"properties": {
"seconds": {
"type": "number",
"description": "Number of seconds to wait"
},
"message": {
"type": "string",
"description": "Message to include in the result"
}
},
"required": ["seconds"]
}
}
})
async def wait(self, seconds: float, message: str = "") -> ToolResult:
"""Wait for the specified number of seconds.
Args:
seconds: Number of seconds to wait
message: Optional message to include in result
Returns:
ToolResult with success status and timing information
"""
try:
# Limit the wait time to a reasonable range
seconds = min(max(0.5, float(seconds)), 10.0)
logger.info(f"WaitTool: Starting wait for {seconds} seconds")
start_time = asyncio.get_event_loop().time()
# Perform the actual wait
await asyncio.sleep(seconds)
end_time = asyncio.get_event_loop().time()
elapsed = end_time - start_time
logger.info(f"WaitTool: Completed wait of {elapsed:.2f} seconds")
# Format the result
if message:
result = f"Waited for {elapsed:.2f} seconds with message: {message}"
else:
result = f"Waited for {elapsed:.2f} seconds"
return self.success_response(result)
except Exception as e:
logger.error(f"WaitTool error: {str(e)}")
return self.fail_response(f"Error during wait operation: {str(e)}")
@xml_schema(
tag_name="wait-sequence",
mappings=[
{"param_name": "count", "node_type": "attribute", "path": "."},
{"param_name": "seconds", "node_type": "attribute", "path": "."},
{"param_name": "label", "node_type": "attribute", "path": "."}
],
example='''
<wait-sequence count="3" seconds="1" label="Test" />
'''
)
@openapi_schema({
"type": "function",
"function": {
"name": "wait_sequence",
"description": "Execute a sequence of waits with the same duration",
"parameters": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"description": "Number of sequential waits to perform"
},
"seconds": {
"type": "number",
"description": "Duration of each wait in seconds"
},
"label": {
"type": "string",
"description": "Label to identify this wait sequence"
}
},
"required": ["count", "seconds"]
}
}
})
async def wait_sequence(self, count: int, seconds: float, label: str = "Sequence") -> ToolResult:
"""Perform a sequence of waits with progress reporting.
Args:
count: Number of sequential waits to perform
seconds: Duration of each wait in seconds
label: Label to identify this wait sequence
Returns:
ToolResult with success status and sequence information
"""
try:
# Validate and limit parameters
count = min(max(1, int(count)), 5)
seconds = min(max(0.5, float(seconds)), 5.0)
logger.info(f"WaitTool: Starting wait sequence '{label}' with {count} iterations of {seconds}s each")
# Perform the sequential waits
result_parts = []
total_time = 0
for i in range(count):
start = asyncio.get_event_loop().time()
# Log the wait start
logger.info(f"WaitTool: Sequence '{label}' - Starting iteration {i+1}/{count}")
# Perform the wait
await asyncio.sleep(seconds)
# Calculate elapsed time
elapsed = asyncio.get_event_loop().time() - start
total_time += elapsed
# Add to results
result_parts.append(f"Step {i+1}/{count}: Waited {elapsed:.2f}s")
logger.info(f"WaitTool: Sequence '{label}' - Completed iteration {i+1}/{count} in {elapsed:.2f}s")
# Compile the final result
result = f"Wait Sequence '{label}' completed in {total_time:.2f} seconds:\n"
result += "\n".join(result_parts)
return self.success_response(result)
except Exception as e:
logger.error(f"WaitTool error in sequence '{label}': {str(e)}")
return self.fail_response(f"Error during wait sequence: {str(e)}")

View File

@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Innovative Solutions | Transform Your Business</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<div class="logo">
<h1>InnovateTech</h1>
</div>
<ul class="nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#benefits">Benefits</a></li>
<li><a href="#testimonials">Testimonials</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#contact" class="cta-button">Get Started</a></li>
</ul>
</div>
</nav>
<!-- Hero Section -->
<header class="hero">
<div class="container">
<div class="hero-content">
<h1>Transform Your Business with Smart Solutions</h1>
<p>Empower your company with cutting-edge technology and innovative strategies that drive growth.</p>
<div class="cta-buttons">
<a href="#contact" class="primary-button">Start Free Trial</a>
<a href="#demo" class="secondary-button">Watch Demo</a>
</div>
</div>
<div class="hero-image">
<img src="https://via.placeholder.com/600x400" alt="Business Innovation">
</div>
</div>
</header>
<!-- Features Section -->
<section id="features" class="features">
<div class="container">
<h2>Why Choose Us</h2>
<div class="features-grid">
<div class="feature-card">
<i class="fas fa-rocket"></i>
<h3>Fast Implementation</h3>
<p>Get up and running quickly with our streamlined onboarding process.</p>
</div>
<div class="feature-card">
<i class="fas fa-shield-alt"></i>
<h3>Secure & Reliable</h3>
<p>Enterprise-grade security to protect your valuable business data.</p>
</div>
<div class="feature-card">
<i class="fas fa-chart-line"></i>
<h3>Scalable Solution</h3>
<p>Grow your business with a platform that scales with your needs.</p>
</div>
<div class="feature-card">
<i class="fas fa-headset"></i>
<h3>24/7 Support</h3>
<p>Round-the-clock support to help you succeed.</p>
</div>
</div>
</div>
</section>
<!-- Benefits Section -->
<section id="benefits" class="benefits">
<div class="container">
<h2>Benefits That Drive Success</h2>
<div class="benefits-container">
<div class="benefit-content">
<h3>Increase Productivity</h3>
<p>Streamline your workflows and boost team efficiency by up to 50%.</p>
<ul>
<li>Automated task management</li>
<li>Real-time collaboration tools</li>
<li>Integrated communication systems</li>
</ul>
</div>
<div class="benefit-image">
<img src="https://via.placeholder.com/500x300" alt="Productivity Benefits">
</div>
</div>
</div>
</section>
<!-- Testimonials Section -->
<section id="testimonials" class="testimonials">
<div class="container">
<h2>What Our Clients Say</h2>
<div class="testimonials-grid">
<div class="testimonial-card">
<div class="testimonial-content">
<p>"This solution has transformed how we operate. Our productivity has increased by 200% since implementation."</p>
<div class="testimonial-author">
<img src="https://via.placeholder.com/60x60" alt="John Smith">
<div>
<h4>John Smith</h4>
<p>CEO, Tech Solutions Inc.</p>
</div>
</div>
</div>
</div>
<div class="testimonial-card">
<div class="testimonial-content">
<p>"The best investment we've made for our business. Customer support is outstanding!"</p>
<div class="testimonial-author">
<img src="https://via.placeholder.com/60x60" alt="Sarah Johnson">
<div>
<h4>Sarah Johnson</h4>
<p>Operations Director, Global Corp</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Pricing Section -->
<section id="pricing" class="pricing">
<div class="container">
<h2>Simple, Transparent Pricing</h2>
<div class="pricing-grid">
<div class="pricing-card">
<h3>Starter</h3>
<div class="price">$29<span>/month</span></div>
<ul>
<li>Up to 5 users</li>
<li>Basic features</li>
<li>Email support</li>
<li>2GB storage</li>
</ul>
<a href="#contact" class="pricing-button">Get Started</a>
</div>
<div class="pricing-card featured">
<h3>Professional</h3>
<div class="price">$99<span>/month</span></div>
<ul>
<li>Up to 20 users</li>
<li>Advanced features</li>
<li>Priority support</li>
<li>10GB storage</li>
</ul>
<a href="#contact" class="pricing-button">Get Started</a>
</div>
<div class="pricing-card">
<h3>Enterprise</h3>
<div class="price">Custom</div>
<ul>
<li>Unlimited users</li>
<li>All features</li>
<li>24/7 support</li>
<li>Unlimited storage</li>
</ul>
<a href="#contact" class="pricing-button">Contact Us</a>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contact" class="contact">
<div class="container">
<h2>Ready to Get Started?</h2>
<div class="contact-container">
<form id="contact-form" class="contact-form">
<div class="form-group">
<input type="text" id="name" name="name" placeholder="Your Name" required>
</div>
<div class="form-group">
<input type="email" id="email" name="email" placeholder="Your Email" required>
</div>
<div class="form-group">
<input type="text" id="company" name="company" placeholder="Company Name">
</div>
<div class="form-group">
<textarea id="message" name="message" placeholder="Your Message" required></textarea>
</div>
<button type="submit" class="submit-button">Get Started</button>
</form>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3>InnovateTech</h3>
<p>Transforming businesses with innovative solutions.</p>
</div>
<div class="footer-section">
<h4>Quick Links</h4>
<ul>
<li><a href="#features">Features</a></li>
<li><a href="#benefits">Benefits</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Contact</h4>
<p>Email: info@innovatetech.com</p>
<p>Phone: (555) 123-4567</p>
</div>
<div class="footer-section">
<h4>Follow Us</h4>
<div class="social-links">
<a href="#"><i class="fab fa-facebook"></i></a>
<a href="#"><i class="fab fa-twitter"></i></a>
<a href="#"><i class="fab fa-linkedin"></i></a>
<a href="#"><i class="fab fa-instagram"></i></a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2024 InnovateTech. All rights reserved.</p>
</div>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

View File

@ -113,6 +113,9 @@ class ResponseProcessor:
# Tool index counter for tracking all tool executions
tool_index = 0
# Track finish reason
finish_reason = None
logger.info(f"Starting to process streaming response for thread {thread_id}")
logger.info(f"Config: XML={config.xml_tool_calling}, Native={config.native_tool_calling}, "
f"Execute on stream={config.execute_on_stream}, Execution strategy={config.tool_execution_strategy}")
@ -121,6 +124,11 @@ class ResponseProcessor:
async for chunk in llm_response:
# Default content to yield
# Check for finish_reason
if hasattr(chunk, 'choices') and chunk.choices and hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason:
finish_reason = chunk.choices[0].finish_reason
logger.info(f"Detected finish_reason: {finish_reason}")
if hasattr(chunk, 'choices') and chunk.choices:
delta = chunk.choices[0].delta if hasattr(chunk.choices[0], 'delta') else None
@ -172,108 +180,109 @@ class ResponseProcessor:
# Immediately continue processing more chunks
# Process native tool calls
if config.native_tool_calling and delta and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tool_call in delta.tool_calls:
# Yield the raw tool call chunk directly to the stream
# Safely extract tool call data even if model_dump isn't available
tool_call_data = {}
# Process native tool calls
if config.native_tool_calling and delta and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tool_call in delta.tool_calls:
# Yield the raw tool call chunk directly to the stream
# Safely extract tool call data even if model_dump isn't available
tool_call_data = {}
if hasattr(tool_call, 'model_dump'):
# Use model_dump if available (OpenAI client)
tool_call_data = tool_call.model_dump()
else:
# Manual extraction if model_dump not available
if hasattr(tool_call, 'id'):
tool_call_data['id'] = tool_call.id
if hasattr(tool_call, 'index'):
tool_call_data['index'] = tool_call.index
if hasattr(tool_call, 'type'):
tool_call_data['type'] = tool_call.type
if hasattr(tool_call, 'function'):
tool_call_data['function'] = {}
if hasattr(tool_call.function, 'name'):
tool_call_data['function']['name'] = tool_call.function.name
if hasattr(tool_call.function, 'arguments'):
# Ensure arguments is a string
tool_call_data['function']['arguments'] = tool_call.function.arguments if isinstance(tool_call.function.arguments, str) else json.dumps(tool_call.function.arguments)
# Yield the chunk data
yield {
"type": "tool_call_chunk",
"tool_call": tool_call_data
}
# Log the tool call chunk for debugging
# logger.debug(f"Yielded native tool call chunk: {tool_call_data}")
if not hasattr(tool_call, 'function'):
continue
if hasattr(tool_call, 'model_dump'):
# Use model_dump if available (OpenAI client)
tool_call_data = tool_call.model_dump()
else:
# Manual extraction if model_dump not available
if hasattr(tool_call, 'id'):
tool_call_data['id'] = tool_call.id
if hasattr(tool_call, 'index'):
tool_call_data['index'] = tool_call.index
if hasattr(tool_call, 'type'):
tool_call_data['type'] = tool_call.type
if hasattr(tool_call, 'function'):
tool_call_data['function'] = {}
if hasattr(tool_call.function, 'name'):
tool_call_data['function']['name'] = tool_call.function.name
if hasattr(tool_call.function, 'arguments'):
tool_call_data['function']['arguments'] = tool_call.function.arguments
# Yield the chunk data
yield {
"type": "tool_call_chunk",
"tool_call": tool_call_data
idx = tool_call.index if hasattr(tool_call, 'index') else 0
# Initialize or update tool call in buffer
if idx not in tool_calls_buffer:
tool_calls_buffer[idx] = {
'id': tool_call.id if hasattr(tool_call, 'id') and tool_call.id else str(uuid.uuid4()),
'type': 'function',
'function': {
'name': tool_call.function.name if hasattr(tool_call.function, 'name') and tool_call.function.name else None,
'arguments': ''
}
}
current_tool = tool_calls_buffer[idx]
if hasattr(tool_call, 'id') and tool_call.id:
current_tool['id'] = tool_call.id
if hasattr(tool_call.function, 'name') and tool_call.function.name:
current_tool['function']['name'] = tool_call.function.name
if hasattr(tool_call.function, 'arguments') and tool_call.function.arguments:
current_tool['function']['arguments'] += tool_call.function.arguments
# Check if we have a complete tool call
has_complete_tool_call = False
if (current_tool['id'] and
current_tool['function']['name'] and
current_tool['function']['arguments']):
try:
json.loads(current_tool['function']['arguments'])
has_complete_tool_call = True
except json.JSONDecodeError:
pass
if has_complete_tool_call and config.execute_tools and config.execute_on_stream:
# Execute this tool call
tool_call_data = {
"function_name": current_tool['function']['name'],
"arguments": json.loads(current_tool['function']['arguments']),
"id": current_tool['id']
}
# Log the tool call chunk for debugging
logger.debug(f"Yielded native tool call chunk: {tool_call_data}")
# Create a context for this tool execution
context = self._create_tool_context(
tool_call=tool_call_data,
tool_index=tool_index
)
if not hasattr(tool_call, 'function'):
continue
idx = tool_call.index if hasattr(tool_call, 'index') else 0
# Yield tool execution start message
yield self._yield_tool_started(context)
# Initialize or update tool call in buffer
if idx not in tool_calls_buffer:
tool_calls_buffer[idx] = {
'id': tool_call.id if hasattr(tool_call, 'id') and tool_call.id else str(uuid.uuid4()),
'type': 'function',
'function': {
'name': tool_call.function.name if hasattr(tool_call.function, 'name') and tool_call.function.name else None,
'arguments': ''
}
}
# Start tool execution as a background task
execution_task = asyncio.create_task(self._execute_tool(tool_call_data))
current_tool = tool_calls_buffer[idx]
if hasattr(tool_call, 'id') and tool_call.id:
current_tool['id'] = tool_call.id
if hasattr(tool_call.function, 'name') and tool_call.function.name:
current_tool['function']['name'] = tool_call.function.name
if hasattr(tool_call.function, 'arguments') and tool_call.function.arguments:
current_tool['function']['arguments'] += tool_call.function.arguments
# Store the task for later retrieval
pending_tool_executions.append({
"task": execution_task,
"tool_call": tool_call_data,
"tool_index": tool_index,
"context": context
})
# Check if we have a complete tool call
has_complete_tool_call = False
if (current_tool['id'] and
current_tool['function']['name'] and
current_tool['function']['arguments']):
try:
json.loads(current_tool['function']['arguments'])
has_complete_tool_call = True
except json.JSONDecodeError:
pass
# Increment the tool index
tool_index += 1
if has_complete_tool_call and config.execute_tools and config.execute_on_stream:
# Execute this tool call
tool_call_data = {
"function_name": current_tool['function']['name'],
"arguments": json.loads(current_tool['function']['arguments']),
"id": current_tool['id']
}
# Create a context for this tool execution
context = self._create_tool_context(
tool_call=tool_call_data,
tool_index=tool_index
)
# Yield tool execution start message
yield self._yield_tool_started(context)
# Start tool execution as a background task
execution_task = asyncio.create_task(self._execute_tool(tool_call_data))
# Store the task for later retrieval
pending_tool_executions.append({
"task": execution_task,
"tool_call": tool_call_data,
"tool_index": tool_index,
"context": context
})
# Increment the tool index
tool_index += 1
# Immediately continue processing more chunks
# Immediately continue processing more chunks
# Check for completed tool executions
completed_executions = []
for i, execution in enumerate(pending_tool_executions):
@ -485,6 +494,13 @@ class ResponseProcessor:
# Increment tool index for next tool
tool_index += 1
# Finally, if we detected a finish reason, yield it
if finish_reason:
yield {
"type": "finish",
"finish_reason": finish_reason
}
except Exception as e:
logger.error(f"Error processing stream: {str(e)}", exc_info=True)
@ -542,7 +558,7 @@ class ResponseProcessor:
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments
"arguments": tool_call.function.arguments if isinstance(tool_call.function.arguments, str) else json.dumps(tool_call.function.arguments)
}
})
@ -1001,7 +1017,48 @@ class ResponseProcessor:
):
"""Add a tool result to the thread based on the specified format."""
try:
# Always add results as user or assistant messages, never as 'tool' role
# Check if this is a native function call (has id field)
if "id" in tool_call:
# Format as a proper tool message according to OpenAI spec
function_name = tool_call.get("function_name", "")
# Format the tool result content - tool role needs string content
if isinstance(result, str):
content = result
elif hasattr(result, 'output'):
# If it's a ToolResult object
if isinstance(result.output, dict) or isinstance(result.output, list):
# If output is already a dict or list, convert to JSON string
content = json.dumps(result.output)
else:
# Otherwise just use the string representation
content = str(result.output)
else:
# Fallback to string representation of the whole result
content = str(result)
logger.info(f"Formatted tool result content: {content[:100]}...")
# Create the tool response message with proper format
tool_message = {
"role": "tool",
"tool_call_id": tool_call["id"],
"name": function_name,
"content": content
}
logger.info(f"Adding native tool result for tool_call_id={tool_call['id']} with role=tool")
# Add as a tool message
await self.add_message(
thread_id=thread_id,
type="tool", # Special type for tool responses
content=tool_message,
is_llm_message=True
)
return
# For XML and other non-native tools, continue with the original logic
# Determine message role based on strategy
result_role = "user" if strategy == "user_message" else "assistant"
@ -1040,7 +1097,6 @@ class ResponseProcessor:
except Exception as e2:
logger.error(f"Failed even with fallback message: {str(e2)}", exc_info=True)
def _format_xml_tool_result(self, tool_call: Dict[str, Any], result: ToolResult) -> str:
"""Format a tool result as an XML tag or plain text.
@ -1060,7 +1116,6 @@ class ResponseProcessor:
function_name = tool_call["function_name"]
return f"Result for {function_name}: {str(result)}"
# At class level, define a method for yielding tool results
def _yield_tool_result(self, context: ToolExecutionContext) -> Dict[str, Any]:
"""Format and return a tool result message."""

View File

@ -58,7 +58,7 @@ class ThreadManager:
Args:
thread_id: The ID of the thread to add the message to.
type: The type of the message (e.g., 'text', 'image_url', 'tool_call').
type: The type of the message (e.g., 'text', 'image_url', 'tool_call', 'tool', 'user', 'assistant').
content: The content of the message. Can be a dictionary, list, or string.
It will be stored as JSONB in the database.
is_llm_message: Flag indicating if the message originated from the LLM.
@ -115,7 +115,18 @@ class ThreadManager:
logger.error(f"Failed to parse message: {item}")
else:
messages.append(item)
# Ensure tool_calls have properly formatted function arguments
for message in messages:
if message.get('tool_calls'):
for tool_call in message['tool_calls']:
if isinstance(tool_call, dict) and 'function' in tool_call:
# Ensure function.arguments is a string
if 'arguments' in tool_call['function'] and not isinstance(tool_call['function']['arguments'], str):
# Log and fix the issue
logger.warning(f"Found non-string arguments in tool_call, converting to string")
tool_call['function']['arguments'] = json.dumps(tool_call['function']['arguments'])
return messages
except Exception as e:
@ -214,7 +225,70 @@ class ThreadManager:
tool_choice=tool_choice if processor_config.native_tool_calling else None,
stream=stream
)
logger.debug("Successfully received LLM API response")
logger.debug("Successfully received raw LLM API response stream/object")
# # --- BEGIN ADDED DEBUG LOGGING ---
# async def logging_stream_wrapper(response_stream):
# stream_ended = False
# final_chunk_metadata = None
# last_chunk = None # Store the last received chunk
# try:
# chunk_count = 0
# async for chunk in response_stream:
# chunk_count += 1
# last_chunk = chunk # Keep track of the last chunk
# # Try to access potential finish reason or metadata directly from chunk
# finish_reason = None
# if hasattr(chunk, 'choices') and chunk.choices and hasattr(chunk.choices[0], 'finish_reason'):
# finish_reason = chunk.choices[0].finish_reason
# logger.debug(f"--> Raw Chunk {chunk_count}: Type={type(chunk)}, FinishReason={finish_reason}, Content={getattr(chunk.choices[0].delta, 'content', None)}, ToolCalls={getattr(chunk.choices[0].delta, 'tool_calls', None)}")
# # Store metadata if it contains finish_reason
# if finish_reason:
# final_chunk_metadata = {"finish_reason": finish_reason}
# logger.info(f"--> Raw Stream: Detected finish_reason='{finish_reason}' in chunk {chunk_count}")
# yield chunk
# stream_ended = True
# logger.info(f"--> Raw Stream: Finished iterating naturally after {chunk_count} chunks.")
# except Exception as e:
# logger.error(f"--> Raw Stream: Error during iteration: {str(e)}", exc_info=True)
# stream_ended = True # Assume ended on error
# raise
# finally:
# if not stream_ended:
# logger.warning("--> Raw Stream: Exited wrapper unexpectedly (maybe client stopped iterating?)")
# # Log the entire last chunk received
# if last_chunk:
# try:
# # Try converting to dict if it's an object with model_dump
# last_chunk_data = last_chunk.model_dump() if hasattr(last_chunk, 'model_dump') else vars(last_chunk)
# logger.info(f"--> Raw Stream: Last Raw Chunk Received: {last_chunk_data}")
# except Exception as log_ex:
# logger.warning(f"--> Raw Stream: Could not serialize last chunk for logging: {log_ex}")
# logger.info(f"--> Raw Stream: Last Raw Chunk (repr): {repr(last_chunk)}")
# else:
# logger.warning("--> Raw Stream: No chunks were received or stored.")
# # Attempt to get final metadata if stream has an attribute for it (depends on litellm/provider)
# final_metadata = getattr(response_stream, 'response_metadata', {})
# if final_chunk_metadata: # Prioritize finish_reason found in-stream
# final_metadata.update(final_chunk_metadata)
# logger.info(f"--> Raw Stream: Final Metadata (if available): {final_metadata}")
# # Wrap the stream only if it's streaming mode
# if stream and hasattr(raw_llm_response, '__aiter__'):
# llm_response = logging_stream_wrapper(raw_llm_response)
# logger.debug("Wrapped raw LLM stream with logging wrapper.")
# else:
# # If not streaming, just use the raw response (might be a dict/object)
# llm_response = raw_llm_response
# logger.debug("Not wrapping non-streaming LLM response.")
# # --- END ADDED DEBUG LOGGING ---
except Exception as e:
logger.error(f"Failed to make LLM API call: {str(e)}", exc_info=True)
raise

View File

@ -19,6 +19,7 @@ import litellm
from utils.logger import logger
# litellm.set_verbose=True
litellm.modify_params=True
# Constants
MAX_RETRIES = 3
@ -311,4 +312,3 @@ if __name__ == "__main__":
print("\n✅ integration test completed successfully!")
else:
print("\n❌ Bedrock integration test failed!")

View File

@ -94,8 +94,8 @@ async def test_streaming_tool_call():
"""Test tool calling with streaming to observe behavior."""
# Setup conversation
messages = [
{"role": "system", "content": "You are a helpful assistant with access to file management tools."},
{"role": "user", "content": "Create an HTML file named hello.html with a simple Hello World message."}
{"role": "system", "content": "You are a helpful assistant with access to file management tools. YOU ALWAYS USE MULTIPLE TOOL FUNCTION CALLS AT ONCE. YOU NEVER USE ONE TOOL FUNCTION CALL AT A TIME."},
{"role": "user", "content": "Create 10 random files with different extensions and content."}
]
print("\n=== Testing streaming tool call ===\n")
@ -105,10 +105,10 @@ async def test_streaming_tool_call():
print("Sending streaming request...")
stream_response = await make_llm_api_call(
messages=messages,
model_name="gpt-4o",
model_name="anthropic/claude-3-5-sonnet-latest",
temperature=0.0,
tools=[CREATE_FILE_SCHEMA],
tool_choice={"type": "function", "function": {"name": "create_file"}},
tool_choice="auto",
stream=True
)
@ -123,10 +123,12 @@ async def test_streaming_tool_call():
# Storage for accumulated tool calls
tool_calls = []
last_chunk = None # Variable to store the last chunk
# Process each chunk
async for chunk in stream_response:
chunk_count += 1
last_chunk = chunk # Keep track of the last chunk
# Print chunk number and type
print(f"\n--- Chunk {chunk_count} ---")
@ -203,6 +205,24 @@ async def test_streaming_tool_call():
print(f"Error parsing arguments: {str(e)}")
else:
print("\nNo tool calls accumulated from streaming response.")
# --- Added logging for last chunk and finish reason ---
finish_reason = None
if last_chunk:
try:
if hasattr(last_chunk, 'choices') and last_chunk.choices:
finish_reason = last_chunk.choices[0].finish_reason
last_chunk_data = last_chunk.model_dump() if hasattr(last_chunk, 'model_dump') else vars(last_chunk)
print("\n--- Last Chunk Received ---")
print(f"Finish Reason: {finish_reason}")
print(f"Raw Last Chunk Data: {json.dumps(last_chunk_data, indent=2)}")
except Exception as log_ex:
print("\n--- Error logging last chunk ---")
print(f"Error: {log_ex}")
print(f"Last Chunk (repr): {repr(last_chunk)}")
else:
print("\n--- No last chunk recorded ---")
# --- End added logging ---
except Exception as e:
logger.error(f"Error in streaming test: {str(e)}", exc_info=True)