response processor orga refactor

This commit is contained in:
marko-kraemer 2025-04-05 13:25:03 +01:00
parent eea33e967c
commit cf13f3af31
4 changed files with 682 additions and 319 deletions

View File

@ -3,34 +3,493 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flappy Bird Test</title>
<title>Minecraft Clone</title>
<style>
canvas {
border: 1px solid black;
/* Previous CSS styles remain the same */
body { margin: 0; overflow: hidden; }
canvas { display: block; image-rendering: pixelated; }
.crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
pointer-events: none;
text-shadow: 2px 2px #000;
}
.inventory {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
}
.inventory-slot {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
image-rendering: pixelated;
}
.inventory-slot.selected {
background: rgba(255, 255, 255, 0.4);
border-color: #ffff00;
}
.hud {
position: fixed;
top: 20px;
left: 20px;
color: white;
font-family: 'Minecraft', monospace;
text-shadow: 2px 2px #000;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="320" height="480"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
<div class="crosshair">+</div>
<div class="hud">
<div id="coordinates"></div>
<div id="selected-block"></div>
</div>
<div class="inventory" id="inventory"></div>
// Test bird
const bird = {
x: 50,
y: 200,
size: 20
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script>
// Initialize Three.js with Minecraft-style settings
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x8CB4DB, 30, 100); // Minecraft-style fog distance
scene.background = new THREE.Color(0x8CB4DB); // Minecraft sky blue
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
antialias: false,
logarithmicDepthBuffer: true,
physicallyCorrectLights: true
});
const clock = new THREE.Clock(); // Disable antialiasing for pixelated look
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(1); // Force pixel ratio to 1 for sharp pixels
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Minecraft-style textures
const textureLoader = new THREE.TextureLoader();
const texturePaths = {
grass_top: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/grass_top.png',
grass_side: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/grass_side.png',
dirt: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/dirt.png',
stone: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/stone.png',
wood: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/wood.png',
wood_top: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/wood.png',
leaves: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/leaves.png',
bedrock: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/minecraft/stone.png'
};
// Draw test bird
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.fillRect(bird.x, bird.y, bird.size, bird.size);
// Configure texture settings for pixelated look
function loadMinecraftTexture(path) {
const texture = textureLoader.load(path);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
return texture;
}
draw();
// Load all textures with Minecraft settings
const blockTextures = {};
for (const [key, path] of Object.entries(texturePaths)) {
blockTextures[key] = loadMinecraftTexture(path);
}
// Create materials with Minecraft-style shading
const materialSettings = {
transparent: false,
side: THREE.FrontSide,
shadowSide: THREE.FrontSide,
depthWrite: true,
depthTest: true
};
const blockMaterials = {
grass: [
new THREE.MeshBasicMaterial({ map: blockTextures.grass_side }),
new THREE.MeshBasicMaterial({ map: blockTextures.grass_side }),
new THREE.MeshBasicMaterial({ map: blockTextures.grass_top }),
new THREE.MeshBasicMaterial({ map: blockTextures.dirt }),
new THREE.MeshBasicMaterial({ map: blockTextures.grass_side }),
new THREE.MeshBasicMaterial({ map: blockTextures.grass_side })
],
dirt: Array(6).fill(new THREE.MeshBasicMaterial({ map: blockTextures.dirt })),
stone: Array(6).fill(new THREE.MeshBasicMaterial({ map: blockTextures.stone })),
wood: [
new THREE.MeshBasicMaterial({ map: blockTextures.wood }),
new THREE.MeshBasicMaterial({ map: blockTextures.wood }),
new THREE.MeshBasicMaterial({ map: blockTextures.wood_top }),
new THREE.MeshBasicMaterial({ map: blockTextures.wood_top }),
new THREE.MeshBasicMaterial({ map: blockTextures.wood }),
new THREE.MeshBasicMaterial({ map: blockTextures.wood })
],
leaves: Array(6).fill(new THREE.MeshBasicMaterial({
map: blockTextures.leaves,
transparent: true,
alphaTest: 0.5
})),
bedrock: Array(6).fill(new THREE.MeshBasicMaterial({ map: blockTextures.bedrock }))
};
// Simple lighting for Minecraft-style rendering
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
scene.add(ambientLight);
// Enhanced block creation with Minecraft materials
function createBlock(x, y, z, type = 'grass') {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const materials = blockMaterials[type];
const cube = new THREE.Mesh(geometry, materials);
cube.position.set(x, y, z);
cube.castShadow = true;
cube.receiveShadow = true;
cube.userData.type = type;
scene.add(cube);
blocks.set(`${x},${y},${z}`, cube);
return cube;
}
// Improved terrain generation with more Minecraft-like features
const noise = new SimplexNoise();
const worldSize = 32;
const blocks = new Map();
function generateTerrain() {
// Generate bedrock layer
for (let x = -worldSize/2; x < worldSize/2; x++) {
for (let z = -worldSize/2; z < worldSize/2; z++) {
createBlock(x, 0, z, 'bedrock');
}
}
// Generate terrain
for (let x = -worldSize/2; x < worldSize/2; x++) {
for (let z = -worldSize/2; z < worldSize/2; z++) {
const height = Math.floor(
(noise.noise2D(x * 0.1, z * 0.1) + 1) * 5 + 5
);
// Generate layers
for (let y = 1; y < height; y++) {
const blockType = y === height - 1 ? 'grass' :
y > height - 4 ? 'dirt' : 'stone';
createBlock(x, y, z, blockType);
}
// Generate trees with better distribution
if (Math.random() < 0.02 && height > 3) {
const treeHeight = 4 + Math.floor(Math.random() * 3);
// Tree trunk
for (let y = height; y < height + treeHeight; y++) {
createBlock(x, y, z, 'wood');
}
// Tree leaves
for (let lx = -2; lx <= 2; lx++) {
for (let ly = -2; ly <= 2; ly++) {
for (let lz = -2; lz <= 2; lz++) {
if (Math.abs(lx) + Math.abs(ly) + Math.abs(lz) <= 4 && Math.random() < 0.7) {
createBlock(
x + lx,
height + treeHeight + ly,
z + lz,
'leaves'
);
}
}
}
}
}
}
}
}
// Player Controls and Physics
const controls = new THREE.PointerLockControls(camera, document.body);
let canJump = false;
let velocity = new THREE.Vector3();
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
let playerHeight = 1.6;
camera.position.set(0, playerHeight + 20, 20); // Move camera back for better initial view
// Click to start
document.addEventListener('click', () => {
controls.lock();
});
controls.addEventListener('lock', () => {
document.getElementById('inventory').style.display = 'flex';
});
controls.addEventListener('unlock', () => {
document.getElementById('inventory').style.display = 'none';
});
// Keyboard controls
const onKeyDown = function(event) {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
moveForward = true;
break;
case 'ArrowDown':
case 'KeyS':
moveBackward = true;
break;
case 'ArrowLeft':
case 'KeyA':
moveLeft = true;
break;
case 'ArrowRight':
case 'KeyD':
moveRight = true;
break;
case 'Space':
if (canJump) {
velocity.y = 8;
canJump = false;
}
break;
}
};
const onKeyUp = function(event) {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
moveForward = false;
break;
case 'ArrowDown':
case 'KeyS':
moveBackward = false;
break;
case 'ArrowLeft':
case 'KeyA':
moveLeft = false;
break;
case 'ArrowRight':
case 'KeyD':
moveRight = false;
break;
}
};
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
// Inventory system
const inventory = {
selectedSlot: 0,
slots: [
{ type: 'grass', count: Infinity },
{ type: 'dirt', count: Infinity },
{ type: 'stone', count: Infinity },
{ type: 'wood', count: Infinity },
{ type: 'leaves', count: Infinity },
{ type: 'bedrock', count: Infinity }
]
};
function updateInventoryUI() {
const inventoryEl = document.getElementById('inventory');
inventoryEl.innerHTML = '';
inventory.slots.forEach((slot, index) => {
const slotEl = document.createElement('div');
slotEl.className = `inventory-slot ${index === inventory.selectedSlot ? 'selected' : ''}`;
slotEl.textContent = slot.type;
slotEl.onclick = () => {
inventory.selectedSlot = index;
updateInventoryUI();
};
inventoryEl.appendChild(slotEl);
});
}
// Mouse wheel to change selected slot
document.addEventListener('wheel', (event) => {
if (controls.isLocked) {
inventory.selectedSlot = (inventory.selectedSlot + (event.deltaY > 0 ? 1 : -1) + inventory.slots.length) % inventory.slots.length;
updateInventoryUI();
}
});
// Block interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Place block
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
if (controls.isLocked) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const intersect = intersects[0];
const position = intersect.point.add(intersect.face.normal);
const roundedPos = position.round();
// Check if block placement is not too close to player
const playerPos = camera.position.clone();
if (playerPos.distanceTo(roundedPos) > 2) {
const selectedType = inventory.slots[inventory.selectedSlot].type;
createBlock(
roundedPos.x,
roundedPos.y,
roundedPos.z,
selectedType
);
}
}
}
});
// Break block
document.addEventListener('click', (event) => {
if (controls.isLocked) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const intersect = intersects[0];
const block = intersect.object;
// Prevent breaking bedrock
if (block.userData.type !== 'bedrock') {
scene.remove(block);
blocks.delete(`${block.position.x},${block.position.y},${block.position.z}`);
}
}
}
});
// Player physics and movement
function movePlayer(delta) {
if (controls.isLocked) {
// Gravity
velocity.y -= 20 * delta; // Minecraft-like gravity
// Forward/backward movement
const direction = new THREE.Vector3();
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveRight) - Number(moveLeft);
direction.normalize();
// Apply movement based on camera direction
if (moveForward || moveBackward) {
velocity.z -= direction.z * 20 * delta;
}
if (moveLeft || moveRight) {
velocity.x -= direction.x * 20 * delta;
}
// Apply friction
velocity.x *= 0.9;
velocity.z *= 0.9;
// Collision detection
const futurePosition = camera.position.clone();
futurePosition.x += velocity.x * delta;
futurePosition.y += velocity.y * delta;
futurePosition.z += velocity.z * delta;
// Check for collisions with blocks
const playerRadius = 0.3;
const checkPositions = [
[0, 0, 0], // Center
[playerRadius, 0, playerRadius], // Corners
[-playerRadius, 0, playerRadius],
[playerRadius, 0, -playerRadius],
[-playerRadius, 0, -playerRadius]
];
let collision = false;
for (const [offsetX, offsetY, offsetZ] of checkPositions) {
const checkPos = futurePosition.clone().add(new THREE.Vector3(offsetX, offsetY, offsetZ));
const blockPos = checkPos.clone().floor();
if (blocks.has(`${blockPos.x},${blockPos.y},${blockPos.z}`)) {
collision = true;
break;
}
}
// Update position if no collision
if (!collision) {
controls.moveRight(-velocity.x * delta);
controls.moveForward(-velocity.z * delta);
camera.position.y += velocity.y * delta;
} else {
velocity.setX(0);
velocity.setZ(0);
if (velocity.y < 0) {
velocity.setY(0);
canJump = true;
}
}
// Prevent falling through ground
if (camera.position.y < playerHeight) {
camera.position.y = playerHeight;
velocity.y = 0;
canJump = true;
}
// Update HUD
document.getElementById('coordinates').textContent =
`Position: ${Math.round(camera.position.x)}, ${Math.round(camera.position.y)}, ${Math.round(camera.position.z)}`;
document.getElementById('selected-block').textContent =
`Selected: ${inventory.slots[inventory.selectedSlot].type}`;
}
}
// ... (previous code for controls, movement, and interaction remains unchanged)
// Update animation loop with fog
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(0.1, clock.getDelta());
movePlayer(delta);
// Update fog based on time of day (simplified)
const time = Date.now() * 0.001;
const fogColor = new THREE.Color(0x8CB4DB);
scene.fog.color = fogColor;
scene.background = fogColor;
renderer.render(scene, camera);
}
// Initialize the game
generateTerrain();
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

View File

@ -13,7 +13,7 @@ import asyncio
import re
import uuid
from typing import List, Dict, Any, Optional, Tuple, AsyncGenerator, Callable, Union, Literal
from dataclasses import dataclass
from dataclasses import dataclass, field
from agentpress.tool import Tool, ToolResult
from agentpress.tool_registry import ToolRegistry
@ -25,6 +25,15 @@ XmlAddingStrategy = Literal["user_message", "assistant_message", "inline_edit"]
# Type alias for tool execution strategy
ToolExecutionStrategy = Literal["sequential", "parallel"]
@dataclass
class ToolExecutionContext:
"""Context for a tool execution including call details, result, and display info."""
tool_call: Dict[str, Any]
tool_index: int
result: Optional[ToolResult] = None
display_name: Optional[str] = None
error: Optional[Exception] = None
@dataclass
class ProcessorConfig:
"""
@ -103,6 +112,9 @@ class ResponseProcessor:
# Tool execution index counter
tool_execution_index = 0
# Tool index for remaining tools (used at end of function)
tool_index = 0
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}")
@ -110,7 +122,6 @@ class ResponseProcessor:
try:
async for chunk in llm_response:
# Default content to yield
content_to_yield = None
if hasattr(chunk, 'choices') and chunk.choices:
delta = chunk.choices[0].delta if hasattr(chunk.choices[0], 'delta') else None
@ -136,16 +147,16 @@ class ResponseProcessor:
# Parse and extract the tool call
tool_call = self._parse_xml_tool_call(xml_chunk)
if tool_call:
# Create a context for this tool execution
context = self._create_tool_context(
tool_call=tool_call,
tool_index=tool_execution_index
)
# Execute tool if needed, but in background
if config.execute_tools and config.execute_on_stream:
# Yield tool execution start message
yield {
"type": "tool_status",
"status": "started",
"name": tool_call["name"],
"message": f"Starting execution of {tool_call['name']}",
"tool_index": tool_execution_index
}
yield self._yield_tool_started(context)
# Start tool execution as a background task
execution_task = asyncio.create_task(self._execute_tool(tool_call))
@ -154,7 +165,8 @@ class ResponseProcessor:
pending_tool_executions.append({
"task": execution_task,
"tool_call": tool_call,
"tool_index": tool_execution_index
"tool_index": tool_execution_index,
"context": context
})
# Increment the tool execution index
@ -208,14 +220,14 @@ class ResponseProcessor:
"id": current_tool['id']
}
# Create a context for this tool execution
context = self._create_tool_context(
tool_call=tool_call_data,
tool_index=tool_execution_index
)
# Yield tool execution start message
yield {
"type": "tool_status",
"status": "started",
"name": tool_call_data["name"],
"message": f"Starting execution of {tool_call_data['name']}",
"tool_index": tool_execution_index
}
yield self._yield_tool_started(context)
# Start tool execution as a background task
execution_task = asyncio.create_task(self._execute_tool(tool_call_data))
@ -224,7 +236,8 @@ class ResponseProcessor:
pending_tool_executions.append({
"task": execution_task,
"tool_call": tool_call_data,
"tool_index": tool_execution_index
"tool_index": tool_execution_index,
"context": context
})
# Increment the tool execution index
@ -245,22 +258,19 @@ class ResponseProcessor:
# Store result for later database updates
tool_results_buffer.append((tool_call, result))
# Yield tool status message first
yield {
"type": "tool_status",
"status": "completed" if result.success else "failed",
"name": tool_call["name"],
"message": f"Tool {tool_call['name']} {'completed successfully' if result.success else 'failed'}",
"tool_index": tool_index
}
# Get or create the context
if "context" in execution:
context = execution["context"]
context.result = result
else:
context = self._create_tool_context(tool_call, tool_index)
context.result = result
# Yield tool execution result for client display
yield {
"type": "tool_result",
"name": tool_call["name"],
"result": str(result),
"tool_index": tool_index
}
# Yield tool status message first
yield self._yield_tool_completed(context)
# Yield tool execution result
yield self._yield_tool_result(context)
# Mark for removal
completed_executions.append(i)
@ -270,14 +280,16 @@ class ResponseProcessor:
tool_call = execution["tool_call"]
tool_index = execution.get("tool_index", -1)
# Yield error status
yield {
"type": "tool_status",
"status": "error",
"name": tool_call["name"],
"message": f"Error executing tool: {str(e)}",
"tool_index": tool_index
}
# Get or create the context
if "context" in execution:
context = execution["context"]
context.error = e
else:
context = self._create_tool_context(tool_call, tool_index)
context.error = e
# Yield error status for the tool
yield self._yield_tool_error(context)
# Mark for removal
completed_executions.append(i)
@ -305,33 +317,37 @@ class ResponseProcessor:
# Store result for later
tool_results_buffer.append((tool_call, result))
# Get or create the context
if "context" in execution:
context = execution["context"]
context.result = result
else:
context = self._create_tool_context(tool_call, tool_index)
context.result = result
# Yield tool status message first
yield {
"type": "tool_status",
"status": "completed" if result.success else "failed",
"name": tool_call["name"],
"message": f"Tool {tool_call['name']} {'completed successfully' if result.success else 'failed'}",
"tool_index": tool_index
}
yield self._yield_tool_completed(context)
# Yield tool execution result
yield {
"type": "tool_result",
"name": tool_call["name"],
"result": str(result),
"tool_index": tool_index
}
yield self._yield_tool_result(context)
except Exception as e:
logger.error(f"Error processing remaining tool execution: {str(e)}")
# Yield error status for the tool
if "tool_call" in execution:
tool_call = execution["tool_call"]
tool_index = execution.get("tool_index", -1)
# Get or create the context
if "context" in execution:
context = execution["context"]
context.error = e
else:
context = self._create_tool_context(tool_call, tool_index)
context.error = e
formatted_result = self._format_xml_tool_result(tool_call, result)
yield {
"type": "tool_status",
"status": "error",
"name": tool_call.get("name", "unknown"),
"message": f"Error processing tool result: {str(e)}",
"type": "tool_result",
"name": context.display_name,
"result": formatted_result,
"tool_index": tool_index
}
@ -374,6 +390,16 @@ class ResponseProcessor:
config.xml_adding_strategy
)
# Create context for tool result
context = self._create_tool_context(tool_call, tool_index)
context.result = result
# Yield tool execution result
yield self._yield_tool_result(context)
# Increment tool index for next tool
tool_index += 1
# Execute any remaining tool calls if not done during streaming
if config.execute_tools and not config.execute_on_stream:
tool_calls_to_execute = []
@ -414,12 +440,15 @@ class ResponseProcessor:
config.xml_adding_strategy
)
# Create context for tool result
context = self._create_tool_context(tool_call, tool_index)
context.result = result
# Yield tool execution result
yield {
"type": "tool_result",
"name": tool_call["name"],
"result": str(result)
}
yield self._yield_tool_result(context)
# Increment tool index for next tool
tool_index += 1
except Exception as e:
logger.error(f"Error processing stream: {str(e)}", exc_info=True)
@ -445,6 +474,8 @@ class ResponseProcessor:
# Extract content and tool calls from response
content = ""
tool_calls = []
# Tool execution counter
tool_index = 0
if hasattr(llm_response, 'choices') and llm_response.choices:
response_message = llm_response.choices[0].message if hasattr(llm_response.choices[0], 'message') else None
@ -509,12 +540,15 @@ class ResponseProcessor:
config.xml_adding_strategy
)
# Create context for tool result
context = self._create_tool_context(tool_call, tool_index)
context.result = result
# Yield tool execution result
yield {
"type": "tool_result",
"name": tool_call["name"],
"result": str(result)
}
yield self._yield_tool_result(context)
# Increment tool index for next tool
tool_index += 1
except Exception as e:
logger.error(f"Error processing response: {str(e)}", exc_info=True)
@ -924,18 +958,12 @@ class ResponseProcessor:
# Determine message role based on strategy
result_role = "user" if strategy == "user_message" else "assistant"
# Determine if this is an XML tool
is_xml_tool = False
if hasattr(self.tool_registry, 'xml_tools'):
is_xml_tool = tool_call["name"] in self.tool_registry.xml_tools
# Create a context for consistent formatting
context = self._create_tool_context(tool_call, 0) # Index doesn't matter for DB
context.result = result
# Format the content based on tool type
if is_xml_tool:
# For XML tools, use simple content
content = str(result)
else:
# For native tools, include context about which tool was called
content = f"Result for {tool_call['name']}: {str(result)}"
# Format the content using the formatting helper
content = self._format_xml_tool_result(tool_call, result)
# Add the message with the appropriate role
result_message = {
@ -953,3 +981,99 @@ 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 a simple XML tag.
Args:
tool_call: The tool call that was executed
result: The result of the tool execution
Returns:
String containing the XML-formatted result or standard format if not XML
"""
xml_tag_name = self._get_tool_display_name(tool_call["name"])
# If display name is same as method name, it's not an XML tool
if xml_tag_name == tool_call["name"]:
return f"Result for {tool_call['name']}: {str(result)}"
# Format as simple XML tag without attributes
xml_output = f"<{xml_tag_name}> {str(result)} </{xml_tag_name}>"
return xml_output
def _get_tool_display_name(self, method_name: str) -> str:
"""Get the display name for a tool (XML tag name if applicable, or method name)."""
if not hasattr(self.tool_registry, 'xml_tools'):
return method_name
# Check if this method corresponds to an XML tool
for tag_name, xml_tool_info in self.tool_registry.xml_tools.items():
if xml_tool_info.get('method') == method_name:
return tag_name
# Default to the method name if no XML tag found
return method_name
# 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."""
if not context.result:
return {
"type": "tool_result",
"name": context.display_name,
"result": "No result available",
"tool_index": context.tool_index
}
formatted_result = self._format_xml_tool_result(context.tool_call, context.result)
return {
"type": "tool_result",
"name": context.display_name,
"result": formatted_result,
"tool_index": context.tool_index
}
def _create_tool_context(self, tool_call: Dict[str, Any], tool_index: int) -> ToolExecutionContext:
"""Create a tool execution context with display name populated."""
context = ToolExecutionContext(
tool_call=tool_call,
tool_index=tool_index
)
context.display_name = self._get_tool_display_name(tool_call["name"])
return context
def _yield_tool_started(self, context: ToolExecutionContext) -> Dict[str, Any]:
"""Format and return a tool started status message."""
return {
"type": "tool_status",
"status": "started",
"name": context.display_name,
"message": f"Starting execution of {context.display_name}",
"tool_index": context.tool_index
}
def _yield_tool_completed(self, context: ToolExecutionContext) -> Dict[str, Any]:
"""Format and return a tool completed/failed status message."""
if not context.result:
return self._yield_tool_error(context)
return {
"type": "tool_status",
"status": "completed" if context.result.success else "failed",
"name": context.display_name,
"message": f"Tool {context.display_name} {'completed successfully' if context.result.success else 'failed'}",
"tool_index": context.tool_index
}
def _yield_tool_error(self, context: ToolExecutionContext) -> Dict[str, Any]:
"""Format and return a tool error status message."""
error_msg = str(context.error) if context.error else "Unknown error"
return {
"type": "tool_status",
"status": "error",
"name": context.display_name,
"message": f"Error executing tool: {error_msg}",
"tool_index": context.tool_index
}

View File

@ -1,80 +0,0 @@
"""
Simplified test for sequential vs parallel tool execution.
"""
import os
import asyncio
import sys
from dotenv import load_dotenv
from agentpress.thread_manager import ThreadManager
from agentpress.response_processor import ProcessorConfig
from agent.tools.wait_tool import WaitTool
from agentpress.tool import ToolResult
# Load environment variables
load_dotenv()
async def test_execution():
"""Directly test sequential vs parallel execution."""
print("\n" + "="*80)
print("🧪 TESTING PARALLEL VS SEQUENTIAL EXECUTION")
print("="*80 + "\n")
# Initialize ThreadManager and register tools
thread_manager = ThreadManager()
thread_manager.add_tool(WaitTool)
# Create wait tool calls
wait_tool_calls = [
{"name": "wait", "arguments": {"seconds": 2, "message": "Wait tool 1"}},
{"name": "wait", "arguments": {"seconds": 2, "message": "Wait tool 2"}},
{"name": "wait", "arguments": {"seconds": 2, "message": "Wait tool 3"}}
]
# Test sequential execution
print("🔄 Testing Sequential Execution")
print("-"*60)
sequential_start = asyncio.get_event_loop().time()
sequential_results = await thread_manager.response_processor._execute_tools(
wait_tool_calls,
sequential=True,
execution_strategy="sequential"
)
sequential_end = asyncio.get_event_loop().time()
sequential_time = sequential_end - sequential_start
print(f"Sequential execution completed in {sequential_time:.2f} seconds")
print()
# Test parallel execution
print("⚡ Testing Parallel Execution")
print("-"*60)
parallel_start = asyncio.get_event_loop().time()
parallel_results = await thread_manager.response_processor._execute_tools(
wait_tool_calls,
sequential=False,
execution_strategy="parallel"
)
parallel_end = asyncio.get_event_loop().time()
parallel_time = parallel_end - parallel_start
print(f"Parallel execution completed in {parallel_time:.2f} seconds")
print()
# Report results
print("\n" + "="*80)
print(f"🧮 RESULTS SUMMARY")
print("="*80)
print(f"Sequential: {sequential_time:.2f} seconds")
print(f"Parallel: {parallel_time:.2f} seconds")
print(f"Speedup: {sequential_time/parallel_time:.2f}x faster")
if __name__ == "__main__":
try:
asyncio.run(test_execution())
except KeyboardInterrupt:
print("\n\n❌ Test interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n\n❌ Error during test: {str(e)}")
sys.exit(1)

View File

@ -1,140 +0,0 @@
"""
Test script for demonstrating sequential vs parallel tool execution strategies.
This script creates a conversation thread and tests both execution strategies
with multiple wait tool calls to clearly demonstrate the difference.
"""
import os
import asyncio
import sys
from dotenv import load_dotenv
from agentpress.thread_manager import ThreadManager
from agentpress.response_processor import ProcessorConfig
from agent.tools.wait_tool import WaitTool
# Load environment variables
load_dotenv()
TOOL_XML_SEQUENTIAL = """
Here are some examples of using the wait tool:
<wait seconds="2">This is sequential wait 1</wait>
<wait seconds="2">This is sequential wait 2</wait>
<wait seconds="2">This is sequential wait 3</wait>
Now wait sequence:
<wait-sequence count="3" seconds="1" label="Sequential Test" />
"""
TOOL_XML_PARALLEL = """
Here are some examples of using the wait tool:
<wait seconds="2">This is parallel wait 1</wait>
<wait seconds="2">This is parallel wait 2</wait>
<wait seconds="2">This is parallel wait 3</wait>
Now wait sequence:
<wait-sequence count="3" seconds="1" label="Parallel Test" />
"""
async def test_execution_strategies():
"""Test both sequential and parallel execution strategies."""
print("\n" + "="*80)
print("🧪 TESTING TOOL EXECUTION STRATEGIES")
print("="*80 + "\n")
# Initialize ThreadManager and register tools
thread_manager = ThreadManager()
thread_manager.add_tool(WaitTool)
# Create a test thread
thread_id = await thread_manager.create_thread()
print(f"🧵 Created test thread: {thread_id}\n")
# Add system message
await thread_manager.add_message(
thread_id,
{
"role": "system",
"content": "You are a testing assistant that will execute wait commands."
}
)
# Test both strategies
test_cases = [
{"name": "Sequential", "strategy": "sequential", "content": TOOL_XML_SEQUENTIAL},
{"name": "Parallel", "strategy": "parallel", "content": TOOL_XML_PARALLEL}
]
for test in test_cases:
print("\n" + "-"*60)
print(f"🔍 Testing {test['name']} Execution Strategy")
print("-"*60 + "\n")
# Add special assistant message with tool calls
# This simulates an LLM response with tool calls
await thread_manager.add_message(
thread_id,
{
"role": "assistant",
"content": test["content"]
}
)
start_time = asyncio.get_event_loop().time()
print(f"⏱️ Starting execution with {test['strategy']} strategy at {start_time:.2f}s")
# Process the response with appropriate strategy
config = ProcessorConfig(
xml_tool_calling=True,
native_tool_calling=False,
execute_tools=True,
execute_on_stream=False,
tool_execution_strategy=test["strategy"]
)
# Get the last message to process
messages = await thread_manager.get_messages(thread_id)
last_message = messages[-1]
# Create a simple non-streaming response object
class MockResponse:
def __init__(self, content):
self.choices = [type('obj', (object,), {
'message': type('obj', (object,), {
'content': content
})
})]
mock_response = MockResponse(last_message["content"])
# Process using the response processor
async for chunk in thread_manager.response_processor.process_non_streaming_response(
llm_response=mock_response,
thread_id=thread_id,
config=config
):
if chunk.get('type') == 'tool_result':
elapsed = asyncio.get_event_loop().time() - start_time
print(f"⏱️ [{elapsed:.2f}s] Tool result: {chunk['name']}")
print(f" {chunk['result']}")
print()
end_time = asyncio.get_event_loop().time()
elapsed = end_time - start_time
print(f"\n⏱️ {test['name']} execution completed in {elapsed:.2f} seconds")
print("\n" + "="*80)
print("✅ Testing completed")
print("="*80 + "\n")
if __name__ == "__main__":
try:
asyncio.run(test_execution_strategies())
except KeyboardInterrupt:
print("\n\n❌ Test interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n\n❌ Error during test: {str(e)}")
sys.exit(1)