From d27b814c97de3c2f669efb03a61cdb88dfac88a4 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Sat, 5 Apr 2025 11:55:52 +0100 Subject: [PATCH] tool indexes & msgs in stream --- backend/agent/workspace/assets/particles.js | 74 ++++++++ backend/agent/workspace/assets/sounds.js | 11 ++ backend/agent/workspace/assets/sprites.js | 29 +++ backend/agent/workspace/assets/styles.css | 46 +++++ backend/agent/workspace/game.js | 193 ++++++++++++++++++++ backend/agent/workspace/index.html | 21 +++ backend/agentpress/response_processor.py | 73 +++++++- 7 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 backend/agent/workspace/assets/particles.js create mode 100644 backend/agent/workspace/assets/sounds.js create mode 100644 backend/agent/workspace/assets/sprites.js create mode 100644 backend/agent/workspace/assets/styles.css create mode 100644 backend/agent/workspace/game.js create mode 100644 backend/agent/workspace/index.html diff --git a/backend/agent/workspace/assets/particles.js b/backend/agent/workspace/assets/particles.js new file mode 100644 index 00000000..307d4e3a --- /dev/null +++ b/backend/agent/workspace/assets/particles.js @@ -0,0 +1,74 @@ +class ParticleSystem { + constructor() { + this.particles = []; + } + + createParticle(x, y, color) { + return { + x: x, + y: y, + color: color, + velocity: { + x: (Math.random() - 0.5) * 3, + y: (Math.random() - 0.5) * 3 + }, + size: Math.random() * 3 + 2, + life: 1, + decay: 0.02 + }; + } + + emit(x, y, count, color) { + for (let i = 0; i < count; i++) { + this.particles.push(this.createParticle(x, y, color)); + } + } + + update(ctx) { + for (let i = this.particles.length - 1; i >= 0; i--) { + const p = this.particles[i]; + + p.x += p.velocity.x; + p.y += p.velocity.y; + p.life -= p.decay; + + if (p.life <= 0) { + this.particles.splice(i, 1); + continue; + } + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fillStyle = `${p.color}${Math.floor(p.life * 255).toString(16).padStart(2, '0')}`; + ctx.fill(); + } + } +} + +class Trail { + constructor(maxPoints = 10) { + this.points = []; + this.maxPoints = maxPoints; + } + + addPoint(x, y) { + this.points.unshift({ x, y, alpha: 1 }); + if (this.points.length > this.maxPoints) { + this.points.pop(); + } + } + + draw(ctx) { + ctx.save(); + for (let i = this.points.length - 1; i >= 0; i--) { + const p = this.points[i]; + p.alpha = i / this.points.length; + + ctx.beginPath(); + ctx.arc(p.x, p.y, 15 * p.alpha, 0, Math.PI * 2); + ctx.fillStyle = `rgba(0, 255, 255, ${p.alpha * 0.3})`; + ctx.fill(); + } + ctx.restore(); + } +} \ No newline at end of file diff --git a/backend/agent/workspace/assets/sounds.js b/backend/agent/workspace/assets/sounds.js new file mode 100644 index 00000000..2a07498a --- /dev/null +++ b/backend/agent/workspace/assets/sounds.js @@ -0,0 +1,11 @@ +// Sound effects manager +const Sounds = { + flap: new Audio('assets/sounds/flap.mp3'), + score: new Audio('assets/sounds/score.mp3'), + hit: new Audio('assets/sounds/hit.mp3'), + + play: function(sound) { + this[sound].currentTime = 0; + this[sound].play().catch(err => console.log('Audio play failed:', err)); + } +}; \ No newline at end of file diff --git a/backend/agent/workspace/assets/sprites.js b/backend/agent/workspace/assets/sprites.js new file mode 100644 index 00000000..868de075 --- /dev/null +++ b/backend/agent/workspace/assets/sprites.js @@ -0,0 +1,29 @@ +// Sprite loading and management +const Sprites = { + bird: new Image(), + pipeTop: new Image(), + pipeBottom: new Image(), + background: new Image(), + + load: function() { + return new Promise((resolve) => { + let loadedImages = 0; + const totalImages = 4; + + const checkLoaded = () => { + loadedImages++; + if (loadedImages === totalImages) resolve(); + }; + + this.bird.onload = checkLoaded; + this.pipeTop.onload = checkLoaded; + this.pipeBottom.onload = checkLoaded; + this.background.onload = checkLoaded; + + this.bird.src = 'assets/images/bird.png'; + this.pipeTop.src = 'assets/images/pipe-top.png'; + this.pipeBottom.src = 'assets/images/pipe-bottom.png'; + this.background.src = 'assets/images/background.png'; + }); + } +}; \ No newline at end of file diff --git a/backend/agent/workspace/assets/styles.css b/backend/agent/workspace/assets/styles.css new file mode 100644 index 00000000..7fc0397a --- /dev/null +++ b/backend/agent/workspace/assets/styles.css @@ -0,0 +1,46 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background: #111; + font-family: 'Press Start 2P', cursive; + overflow: hidden; +} + +canvas { + border: 3px solid #0ff; + background: #070721; + box-shadow: 0 0 20px #0ff; +} + +#game-over { + display: none; + position: absolute; + color: #0ff; + font-size: 24px; + text-align: center; + text-shadow: 0 0 10px #0ff; + background: rgba(0, 0, 0, 0.8); + padding: 20px; + border-radius: 10px; + border: 2px solid #0ff; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 10px #0ff; } + 50% { box-shadow: 0 0 20px #0ff, 0 0 30px #f0f; } + 100% { box-shadow: 0 0 10px #0ff; } +} + +.score { + position: absolute; + top: 20px; + color: #0ff; + text-shadow: 0 0 10px #0ff; +} + +#current-score { left: 20px; } +#high-score { right: 20px; } \ No newline at end of file diff --git a/backend/agent/workspace/game.js b/backend/agent/workspace/game.js new file mode 100644 index 00000000..6898897d --- /dev/null +++ b/backend/agent/workspace/game.js @@ -0,0 +1,193 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const gameOverDiv = document.getElementById('game-over'); + +// Game constants +const GRAVITY = 0.4; +const FLAP_SPEED = -7; +const PIPE_SPEED = 2; +const PIPE_GAP = 150; + +// Initialize particle system and trail +const particles = new ParticleSystem(); +const trail = new Trail(); + +// Game state +let bird = { + x: 50, + y: canvas.height / 2, + velocity: 0, + width: 30, + height: 30, + rotation: 0, + wingAngle: 0 +}; + +let pipes = []; +let score = 0; +let gameRunning = false; +let frameCount = 0; +let highScore = localStorage.getItem('highScore') || 0; + +// Neon colors for the bird +const birdColors = { + primary: '#0ff', + secondary: '#f0f', + glow: '#fff' +}; + +function drawNeonBird() { + ctx.save(); + ctx.translate(bird.x, bird.y); + ctx.rotate(bird.rotation * Math.PI / 180); + + // Draw trail + trail.draw(ctx); + + // Draw wing + bird.wingAngle = Math.sin(frameCount * 0.3) * 30; + + // Bird body glow + ctx.shadowBlur = 20; + ctx.shadowColor = birdColors.primary; + + // Main body + ctx.beginPath(); + ctx.ellipse(0, 0, bird.width/2, bird.height/2, 0, 0, Math.PI * 2); + ctx.fillStyle = birdColors.primary; + ctx.fill(); + + // Wing + ctx.save(); + ctx.rotate(bird.wingAngle * Math.PI / 180); + ctx.beginPath(); + ctx.ellipse(0, 0, bird.width/3, bird.height/4, 0, 0, Math.PI); + ctx.fillStyle = birdColors.secondary; + ctx.fill(); + ctx.restore(); + + // Eye + ctx.shadowBlur = 10; + ctx.shadowColor = birdColors.glow; + ctx.beginPath(); + ctx.arc(bird.width/4, -bird.height/6, 3, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; + ctx.fill(); + + ctx.restore(); +} + +function drawNeonPipe(pipe, isTop) { + ctx.save(); + ctx.shadowBlur = 15; + ctx.shadowColor = '#0ff'; + ctx.strokeStyle = '#0ff'; + ctx.lineWidth = 2; + ctx.fillStyle = 'rgba(0, 255, 255, 0.1)'; + + if (isTop) { + ctx.fillRect(pipe.x, pipe.y, pipe.width, pipe.height); + ctx.strokeRect(pipe.x, pipe.y, pipe.width, pipe.height); + } else { + ctx.fillRect(pipe.x, pipe.y, pipe.width, pipe.height); + ctx.strokeRect(pipe.x, pipe.y, pipe.width, pipe.height); + } + ctx.restore(); +} + +// Update the original drawPipes function +function drawPipes() { + pipes.forEach((pipe, index) => { + drawNeonPipe(pipe, index % 2 === 0); + }); +} + +// Update the original handleClick function +function handleClick() { + if (!gameRunning) { + startGame(); + } else { + bird.velocity = FLAP_SPEED; + bird.rotation = -45; + particles.emit(bird.x, bird.y, 5, birdColors.primary); + Sounds.play('flap'); + } +} + +// Update the original updateGame function +function updateGame() { + if (!gameRunning) return; + + // Update bird + bird.velocity += GRAVITY; + bird.y += bird.velocity; + + // Update trail + trail.addPoint(bird.x, bird.y); + + // Update bird rotation + if (bird.velocity > 0) { + bird.rotation += 4; + bird.rotation = Math.min(90, bird.rotation); + } + + // Generate particles while moving + if (frameCount % 3 === 0) { + particles.emit(bird.x - 10, bird.y, 1, birdColors.secondary); + } + + // Rest of the updateGame function remains the same + [... existing updateGame code ...] +} + +// Update the original animate function +function animate() { + if (!gameRunning) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw background with a gradient + const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, '#000'); + gradient.addColorStop(1, '#111'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw grid effect + drawGrid(); + + updateGame(); + drawPipes(); + particles.update(ctx); + drawNeonBird(); + drawScore(); + + frameCount++; + requestAnimationFrame(animate); +} + +// Add new grid effect +function drawGrid() { + ctx.strokeStyle = 'rgba(0, 255, 255, 0.1)'; + ctx.lineWidth = 1; + + const gridSize = 30; + const offset = frameCount % gridSize; + + for (let x = -offset; x < canvas.width; x += gridSize) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, canvas.height); + ctx.stroke(); + } + + for (let y = -offset; y < canvas.height; y += gridSize) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + } +} + +// Start the game +initGame(); \ No newline at end of file diff --git a/backend/agent/workspace/index.html b/backend/agent/workspace/index.html new file mode 100644 index 00000000..06c1c5ef --- /dev/null +++ b/backend/agent/workspace/index.html @@ -0,0 +1,21 @@ + + + + + + Flappy Bird Clone + + + + +
+ Game Over!
+ Score: 0
+ Click to restart +
+ + + + + + \ No newline at end of file diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py index a8ab17d9..be44b331 100644 --- a/backend/agentpress/response_processor.py +++ b/backend/agentpress/response_processor.py @@ -100,6 +100,9 @@ class ResponseProcessor: # For tracking pending tool executions pending_tool_executions = [] + # Tool execution index counter + tool_execution_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}") @@ -135,15 +138,28 @@ class ResponseProcessor: if tool_call: # 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 + } + # Start tool execution as a background task execution_task = asyncio.create_task(self._execute_tool(tool_call)) # Store the task for later retrieval pending_tool_executions.append({ "task": execution_task, - "tool_call": tool_call + "tool_call": tool_call, + "tool_index": tool_execution_index }) + # Increment the tool execution index + tool_execution_index += 1 + # Immediately continue processing more chunks # Process native tool calls @@ -192,15 +208,28 @@ class ResponseProcessor: "id": current_tool['id'] } + # 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 + } + # 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_call": tool_call_data, + "tool_index": tool_execution_index }) + # Increment the tool execution index + tool_execution_index += 1 + # Immediately continue processing more chunks # Check for completed tool executions @@ -215,11 +244,24 @@ class ResponseProcessor: # Store result for later database updates tool_results_buffer.append((tool_call, result)) + # Get the tool index + tool_index = execution.get("tool_index", -1) + + # 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 tool execution result for client display yield { "type": "tool_result", "name": tool_call["name"], - "result": str(result) + "result": str(result), + "tool_index": tool_index } # Mark for removal @@ -250,14 +292,37 @@ class ResponseProcessor: # Store result for later tool_results_buffer.append((tool_call, result)) + # Get the tool index + tool_index = execution.get("tool_index", -1) + + # 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 tool execution result yield { "type": "tool_result", "name": tool_call["name"], - "result": str(result) + "result": str(result), + "tool_index": tool_index } 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_index = execution.get("tool_index", -1) + yield { + "type": "tool_status", + "status": "error", + "name": execution["tool_call"].get("name", "unknown"), + "message": f"Error processing tool result: {str(e)}", + "tool_index": tool_index + } # After streaming completes, process any remaining content and tool calls if accumulated_content: