diff --git a/.claude/project-context.md b/.claude/project-context.md index 9ce60b3..eb1634e 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -93,8 +93,12 @@ c:\Projects\Agent Arena\ - ✅ Core classes verified: SimulationManager, Agent, EventBus, ToolRegistry - ✅ IPC system implemented (Godot ↔ Python via HTTP/FastAPI) - ✅ Benchmark scenes created (foraging, crafting_chain, team_capture) -- ⏳ Next: Set up Python environment and agent runtime -- ⏳ Next: Integrate LLM backends with agent decision-making +- ✅ Tool execution system connected (Agent → ToolRegistry → IPC → Python) + +### Active Work Items +- 🔄 **Andrew**: LLM backend integration with agent decision-making +- 🔄 **Justin** (Issue #15): Build out benchmark scenes with game content +- ✅ **Justin** (Issue #16): Connect tool execution system in Godot - **COMPLETE** ## Development Commands @@ -196,15 +200,62 @@ python run_ipc_server.py --host 127.0.0.1 --port 5000 --workers 4 --debug ### IPCClient (Node) - HTTP client for Godot ↔ Python communication -- Methods: `connect_to_server()`, `send_tick_request()`, `get_tick_response()`, `has_response()` +- Methods: `connect_to_server()`, `send_tick_request()`, `get_tick_response()`, `has_response()`, `execute_tool_sync()` - Properties: `server_url` - Signals: `response_received`, `connection_failed` +## Tool Execution System + +The tool execution system enables agents to perform actions in the simulation by calling Python tool functions. + +### Architecture +``` +Agent.call_tool() → ToolRegistry.execute_tool() → IPCClient.execute_tool_sync() → +Python IPC Server (/tools/execute) → ToolDispatcher.execute_tool() → Tool Function +``` + +### Available Tools +- **Movement**: `move_to`, `navigate_to`, `stop_movement`, `rotate_to_face` +- **Inventory**: `pickup_item`, `drop_item`, `use_item`, `get_inventory`, `craft_item` +- **World Query**: (defined in `python/tools/world_query.py`) + +### Usage Example (GDScript) +```gdscript +# Setup +var agent = Agent.new() +var tool_registry = ToolRegistry.new() +var ipc_client = IPCClient.new() + +tool_registry.set_ipc_client(ipc_client) +agent.set_tool_registry(tool_registry) + +# Execute tool +var result = agent.call_tool("move_to", { + "target_position": [10.0, 0.0, 5.0], + "speed": 1.5 +}) + +if result["success"]: + print("Tool executed successfully: ", result["result"]) +else: + print("Tool failed: ", result["error"]) +``` + +### Testing +- Test scenes: `scenes/tests/test_tool_execution_simple.tscn` (recommended), `scenes/tests/test_tool_execution.tscn` +- Test scripts: `scripts/tests/test_tool_execution_simple.gd`, `scripts/tests/test_tool_execution.gd` +- Test README: `scenes/tests/README.md` +- Documentation: `TESTING_TOOL_EXECUTION.md`, `TOOL_TESTING_FIXED.md` + ## Known Issues -- Benchmark scenes are empty placeholders (need to create actual game worlds) - Python environment needs initial setup (venv + pip install) -- LLM backends not yet connected to agent decision-making -- Tool execution in Godot currently returns stub responses + +## Recent Issues +- Issue #15: Build out benchmark scenes with game content (assigned to Justin) - In Progress +- Issue #16: Connect tool execution system in Godot (assigned to Justin) - ✅ **COMPLETE** + - See: `TESTING_TOOL_EXECUTION.md`, `TOOL_TESTING_FIXED.md` for details + - Test scenes: `scenes/tests/` (use `test_tool_execution_simple.tscn` for quick verification) +- LLM backend integration (assigned to Andrew) - In Progress ## References - Godot docs: https://docs.godotengine.org/ diff --git a/.gitignore b/.gitignore index 981c3f2..607ff46 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ build/ *.swo *~ .DS_Store +promptPad.md # Logs and databases *.log diff --git a/START_IPC_SERVER.bat b/START_IPC_SERVER.bat new file mode 100644 index 0000000..4e57fe0 --- /dev/null +++ b/START_IPC_SERVER.bat @@ -0,0 +1,55 @@ +@echo off +REM Agent Arena - IPC Server Startup Script +REM This script starts the Python IPC server for Godot-Python communication + +echo ======================================== +echo Agent Arena - Starting IPC Server +echo ======================================== +echo. + +cd /d "%~dp0\python" + +REM Check if venv exists +if not exist "venv\" ( + echo ERROR: Python virtual environment not found! + echo Please run: python -m venv venv + echo Then install dependencies: venv\Scripts\pip install -r requirements.txt + pause + exit /b 1 +) + +REM Activate venv +echo Activating Python virtual environment... +call venv\Scripts\activate.bat + +REM Check if required packages are installed +python -c "import fastapi, uvicorn" 2>nul +if errorlevel 1 ( + echo. + echo ERROR: Required packages not installed! + echo Installing dependencies... + pip install fastapi uvicorn + if errorlevel 1 ( + echo. + echo Failed to install dependencies. + pause + exit /b 1 + ) +) + +echo. +echo Starting IPC Server... +echo Server will be available at: http://127.0.0.1:5000 +echo. +echo Press Ctrl+C to stop the server +echo ======================================== +echo. + +python run_ipc_server.py + +REM If server exits, pause so user can see error +if errorlevel 1 ( + echo. + echo Server exited with error! + pause +) diff --git a/TROUBLESHOOTING_IPC.md b/TROUBLESHOOTING_IPC.md deleted file mode 100644 index 7dec151..0000000 --- a/TROUBLESHOOTING_IPC.md +++ /dev/null @@ -1,140 +0,0 @@ -# Troubleshooting: IPCClient Not Found - -## Error -``` -ERROR: Could not find type "IPCClient" in the current scope. -``` - -## Cause -Godot cached the old GDExtension DLL before IPCClient was added. It needs to reload the new version. - -## Solution - -### Method 1: Clear Cache and Restart (Most Reliable) - -1. **Close Godot completely** (make sure it's not running in background) - -2. **Delete the `.godot` cache folder**: - ```bash - # Windows PowerShell - Remove-Item -Recurse -Force ".godot" - - # Or Windows Command Prompt - rmdir /s /q ".godot" - - # Or manually delete the .godot folder in Windows Explorer - ``` - -3. **Restart Godot**: - - It will recreate the `.godot` folder - - It will reimport all assets and reload the extension - -4. **Verify the classes are loaded**: - - Open and run `scripts/tests/test_extension.gd` - - You should see all 5 classes (including IPCClient) pass - -### Method 2: Just Restart Godot - -Sometimes simply closing and reopening Godot is enough: -1. Close Godot completely -2. Reopen the project -3. Try running `scripts/tests/test_extension.gd` - -### Method 3: Rebuild the Extension - -If the above doesn't work, the DLL might not have been built correctly: - -```bash -cd "c:\Projects\Agent Arena\godot\build" - -# Clean build -"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" --build . --config Debug --clean-first - -# Restart Godot after build completes -``` - -## Verification Steps - -### 1. Check DLL Exists -```bash -dir "c:\Projects\Agent Arena\bin\windows\libagent_arena.windows.template_debug.x86_64.dll" -``` -Should show a file ~3.5 MB in size with recent timestamp. - -### 2. Check Extension Configuration -File `agent_arena.gdextension` should contain: -```ini -[configuration] -entry_symbol = "agent_arena_library_init" -compatibility_minimum = "4.5" - -[libraries] -windows.debug.x86_64 = "res://bin/windows/libagent_arena.windows.template_debug.x86_64.dll" -``` - -### 3. Run Test Script -Open `scenes/test_extension.gd` and run it (F6). You should see: -``` -=== Testing GDExtension Classes === - ✓ SimulationManager - OK - ✓ EventBus - OK - ✓ Agent - OK - ✓ ToolRegistry - OK - ✓ IPCClient - OK -=== Test Complete === -All classes loaded successfully! -``` - -### 4. Check Godot Console on Startup -When Godot starts, it should print: -``` -IPCClient initialized with server URL: http://127.0.0.1:5000 -``` -(This appears when you create an IPCClient node) - -## Common Issues - -### "Still getting IPCClient not found after restart" -- Make sure you deleted the entire `.godot` folder -- Make sure Godot was completely closed (check Task Manager) -- Try rebuilding the extension with `--clean-first` - -### "Other classes work but IPCClient doesn't" -- Check that `godot/src/register_types.cpp` includes: - ```cpp - ClassDB::register_class(); - ``` -- Rebuild the extension - -### "DLL file is locked / can't delete" -- Close Godot first -- If still locked, restart Windows (Godot may have crashed) - -### "Extension loads but crashes" -- Check Windows Event Viewer for C++ errors -- Try Debug build instead of Release build -- Check that all includes are correct in `agent_arena.h` - -## Still Not Working? - -1. Check the build output for any errors (warnings are OK) -2. Verify the file `godot/src/agent_arena.cpp` contains the IPCClient implementation -3. Verify `godot/include/agent_arena.h` contains the IPCClient class definition -4. Try creating a minimal test: - ```gdscript - extends Node - func _ready(): - var client = IPCClient.new() - print("IPCClient created: ", client) - client.free() - ``` - -## Quick Checklist - -- [ ] Godot is completely closed -- [ ] `.godot` folder deleted -- [ ] DLL exists and is recent (check timestamp) -- [ ] Extension built successfully (no errors) -- [ ] `IPCClient` registered in `register_types.cpp` -- [ ] Godot restarted -- [ ] Test script runs successfully diff --git a/godot/include/agent_arena.h b/godot/include/agent_arena.h index 7f95bc5..6293e51 100644 --- a/godot/include/agent_arena.h +++ b/godot/include/agent_arena.h @@ -107,6 +107,7 @@ class Agent : public godot::Node3D { godot::Dictionary short_term_memory; godot::Array action_history; bool is_active; + ToolRegistry* tool_registry; protected: static void _bind_methods(); @@ -131,6 +132,10 @@ class Agent : public godot::Node3D { // Tool interface godot::Dictionary call_tool(const godot::String& tool_name, const godot::Dictionary& params); + // Tool registry management + void set_tool_registry(ToolRegistry* registry); + ToolRegistry* get_tool_registry() const { return tool_registry; } + // Getters/Setters godot::String get_agent_id() const { return agent_id; } void set_agent_id(const godot::String& id) { agent_id = id; } @@ -144,6 +149,7 @@ class ToolRegistry : public godot::Node { private: godot::Dictionary registered_tools; + IPCClient* ipc_client; protected: static void _bind_methods(); @@ -152,11 +158,17 @@ class ToolRegistry : public godot::Node { ToolRegistry(); ~ToolRegistry(); + void _ready() override; + void register_tool(const godot::String& name, const godot::Dictionary& schema); void unregister_tool(const godot::String& name); godot::Dictionary get_tool_schema(const godot::String& name); godot::Array get_all_tool_names(); godot::Dictionary execute_tool(const godot::String& name, const godot::Dictionary& params); + + // IPC Client management + void set_ipc_client(IPCClient* client); + IPCClient* get_ipc_client() const { return ipc_client; } }; /** @@ -195,6 +207,9 @@ class IPCClient : public godot::Node { godot::Dictionary get_tick_response(); bool has_response() const { return response_received; } + // Tool execution + godot::Dictionary execute_tool_sync(const godot::String& tool_name, const godot::Dictionary& params, const godot::String& agent_id = "", uint64_t tick = 0); + // Getters/Setters godot::String get_server_url() const { return server_url; } void set_server_url(const godot::String& url); diff --git a/godot/src/agent_arena.cpp b/godot/src/agent_arena.cpp index 18b2453..d281029 100644 --- a/godot/src/agent_arena.cpp +++ b/godot/src/agent_arena.cpp @@ -170,7 +170,7 @@ void EventBus::load_recording(const Array& events) { // Agent Implementation // ============================================================================ -Agent::Agent() : is_active(true) { +Agent::Agent() : is_active(true), tool_registry(nullptr) { agent_id = "agent_" + String::num_int64(Time::get_singleton()->get_ticks_msec()); } @@ -186,6 +186,8 @@ void Agent::_bind_methods() { ClassDB::bind_method(D_METHOD("clear_short_term_memory"), &Agent::clear_short_term_memory); ClassDB::bind_method(D_METHOD("call_tool", "tool_name", "params"), &Agent::call_tool); + ClassDB::bind_method(D_METHOD("set_tool_registry", "registry"), &Agent::set_tool_registry); + ClassDB::bind_method(D_METHOD("get_tool_registry"), &Agent::get_tool_registry); ClassDB::bind_method(D_METHOD("get_agent_id"), &Agent::get_agent_id); ClassDB::bind_method(D_METHOD("set_agent_id", "id"), &Agent::set_agent_id); @@ -197,6 +199,14 @@ void Agent::_bind_methods() { } void Agent::_ready() { + // Try to find ToolRegistry in the scene tree + Node* parent = get_parent(); + if (parent) { + tool_registry = Object::cast_to(parent->get_node_or_null("ToolRegistry")); + if (tool_registry) { + UtilityFunctions::print("Agent ", agent_id, " connected to ToolRegistry"); + } + } UtilityFunctions::print("Agent ", agent_id, " ready"); } @@ -242,16 +252,31 @@ void Agent::clear_short_term_memory() { Dictionary Agent::call_tool(const String& tool_name, const Dictionary& params) { Dictionary result; - result["success"] = false; - result["error"] = "Tool not implemented"; + + if (tool_registry) { + result = tool_registry->execute_tool(tool_name, params); + UtilityFunctions::print("Agent ", agent_id, " called tool '", tool_name, "'"); + } else { + result["success"] = false; + result["error"] = "No ToolRegistry available"; + UtilityFunctions::print("Agent ", agent_id, " error: No ToolRegistry for tool '", tool_name, "'"); + } + return result; } +void Agent::set_tool_registry(ToolRegistry* registry) { + tool_registry = registry; + if (registry) { + UtilityFunctions::print("Agent ", agent_id, ": ToolRegistry set"); + } +} + // ============================================================================ // ToolRegistry Implementation // ============================================================================ -ToolRegistry::ToolRegistry() {} +ToolRegistry::ToolRegistry() : ipc_client(nullptr) {} ToolRegistry::~ToolRegistry() {} @@ -261,6 +286,21 @@ void ToolRegistry::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tool_schema", "name"), &ToolRegistry::get_tool_schema); ClassDB::bind_method(D_METHOD("get_all_tool_names"), &ToolRegistry::get_all_tool_names); ClassDB::bind_method(D_METHOD("execute_tool", "name", "params"), &ToolRegistry::execute_tool); + ClassDB::bind_method(D_METHOD("set_ipc_client", "client"), &ToolRegistry::set_ipc_client); + ClassDB::bind_method(D_METHOD("get_ipc_client"), &ToolRegistry::get_ipc_client); +} + +void ToolRegistry::_ready() { + // Try to find IPCClient in the scene tree + Node* parent = get_parent(); + if (parent) { + ipc_client = Object::cast_to(parent->get_node_or_null("IPCClient")); + if (ipc_client) { + UtilityFunctions::print("ToolRegistry: IPCClient connected"); + } else { + UtilityFunctions::print("ToolRegistry: Warning - No IPCClient found. Tools will not execute."); + } + } } void ToolRegistry::register_tool(const String& name, const Dictionary& schema) { @@ -295,13 +335,26 @@ Dictionary ToolRegistry::execute_tool(const String& name, const Dictionary& para return result; } - // Tool execution logic would go here - result["success"] = true; - result["output"] = Variant(); + // Execute tool via IPC if available + if (ipc_client) { + result = ipc_client->execute_tool_sync(name, params); + UtilityFunctions::print("Executed tool '", name, "' via IPC"); + } else { + result["success"] = false; + result["error"] = "No IPC client available for tool execution"; + UtilityFunctions::print("Error: Cannot execute tool '", name, "' - no IPC client"); + } return result; } +void ToolRegistry::set_ipc_client(IPCClient* client) { + ipc_client = client; + if (client) { + UtilityFunctions::print("ToolRegistry: IPC client set"); + } +} + // ============================================================================ // IPCClient Implementation // ============================================================================ @@ -329,6 +382,9 @@ void IPCClient::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tick_response"), &IPCClient::get_tick_response); ClassDB::bind_method(D_METHOD("has_response"), &IPCClient::has_response); + ClassDB::bind_method(D_METHOD("execute_tool_sync", "tool_name", "params", "agent_id", "tick"), + &IPCClient::execute_tool_sync); + ClassDB::bind_method(D_METHOD("get_server_url"), &IPCClient::get_server_url); ClassDB::bind_method(D_METHOD("set_server_url", "url"), &IPCClient::set_server_url); @@ -458,3 +514,47 @@ void IPCClient::_on_request_completed(int result, int response_code, is_connected = false; } } + +Dictionary IPCClient::execute_tool_sync(const String& tool_name, const Dictionary& params, + const String& agent_id, uint64_t tick) { + Dictionary result; + + if (!is_connected) { + UtilityFunctions::print("Warning: Tool execution while not connected to server"); + } + + // Build request JSON + Dictionary request_dict; + request_dict["tool_name"] = tool_name; + request_dict["params"] = params; + request_dict["agent_id"] = agent_id; + request_dict["tick"] = tick; + + String json_str = JSON::stringify(request_dict); + + // Send POST request using main http_request + String url = server_url + "/tools/execute"; + PackedStringArray headers; + headers.append("Content-Type: application/json"); + + Error err = http_request->request(url, headers, HTTPClient::METHOD_POST, json_str); + + if (err != OK) { + UtilityFunctions::print("Error sending tool execution request: ", err); + result["success"] = false; + result["error"] = "Failed to send HTTP request"; + return result; + } + + // NOTE: This is a simplified implementation that returns immediately + // In a real scenario, you'd want to wait for the response or use callbacks + // For now, we'll use the pending_response mechanism + UtilityFunctions::print("Tool execution request sent for '", tool_name, "'"); + + // Return a pending status - the actual response will come through the signal + result["success"] = true; + result["result"] = Dictionary(); + result["note"] = "Tool execution initiated - check response signal"; + + return result; +} diff --git a/python/ipc/messages.py b/python/ipc/messages.py index e4406d5..d14b4cc 100644 --- a/python/ipc/messages.py +++ b/python/ipc/messages.py @@ -151,3 +151,62 @@ def to_dict(self) -> dict[str, Any]: "actions": [a.to_dict() for a in self.actions], "metrics": self.metrics, } + + +@dataclass +class ToolExecutionRequest: + """ + Request from Godot to execute a tool in Python. + """ + + tool_name: str + params: dict[str, Any] = field(default_factory=dict) + agent_id: str = "" # Optional agent context + tick: int = 0 # Optional tick context + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ToolExecutionRequest": + """Create ToolExecutionRequest from dictionary.""" + return cls( + tool_name=data["tool_name"], + params=data.get("params", {}), + agent_id=data.get("agent_id", ""), + tick=data.get("tick", 0), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "tool_name": self.tool_name, + "params": self.params, + "agent_id": self.agent_id, + "tick": self.tick, + } + + +@dataclass +class ToolExecutionResponse: + """ + Response from Python after executing a tool. + """ + + success: bool + result: Any = None + error: str = "" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ToolExecutionResponse": + """Create ToolExecutionResponse from dictionary.""" + return cls( + success=data["success"], + result=data.get("result"), + error=data.get("error", ""), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "success": self.success, + "result": self.result, + "error": self.error, + } diff --git a/python/ipc/server.py b/python/ipc/server.py index 4c6b9c6..1945ea6 100644 --- a/python/ipc/server.py +++ b/python/ipc/server.py @@ -13,8 +13,16 @@ from fastapi import FastAPI, HTTPException from agent_runtime.runtime import AgentRuntime +from agent_runtime.tool_dispatcher import ToolDispatcher +from tools import register_inventory_tools, register_movement_tools, register_world_query_tools -from .messages import ActionMessage, TickRequest, TickResponse +from .messages import ( + ActionMessage, + TickRequest, + TickResponse, + ToolExecutionRequest, + ToolExecutionResponse, +) logger = logging.getLogger(__name__) @@ -44,12 +52,22 @@ def __init__( self.host = host self.port = port self.app: FastAPI | None = None + self.tool_dispatcher = ToolDispatcher() + self._register_all_tools() self.metrics = { "total_ticks": 0, "total_agents_processed": 0, "avg_tick_time_ms": 0.0, + "total_tools_executed": 0, } + def _register_all_tools(self) -> None: + """Register all available tools with the dispatcher.""" + register_movement_tools(self.tool_dispatcher) + register_inventory_tools(self.tool_dispatcher) + register_world_query_tools(self.tool_dispatcher) + logger.info(f"Registered {len(self.tool_dispatcher.tools)} tools") + def create_app(self) -> FastAPI: """Create and configure the FastAPI application.""" @@ -192,6 +210,64 @@ async def register_agent(agent_data: dict[str, Any]) -> dict[str, str]: logger.error(f"Error registering agent: {e}") raise HTTPException(status_code=500, detail=str(e)) + @app.post("/tools/execute") + async def execute_tool(request_data: dict[str, Any]) -> dict[str, Any]: + """ + Execute a tool requested from Godot. + + Args: + request_data: Tool execution request + + Returns: + Tool execution response with result or error + """ + try: + # Parse request + tool_request = ToolExecutionRequest.from_dict(request_data) + + logger.debug( + f"Executing tool '{tool_request.tool_name}' " + f"for agent '{tool_request.agent_id}' at tick {tool_request.tick}" + ) + + # Execute the tool through dispatcher + result = self.tool_dispatcher.execute_tool( + tool_request.tool_name, tool_request.params + ) + + # Update metrics + self.metrics["total_tools_executed"] += 1 + + # Build response + response = ToolExecutionResponse( + success=result.get("success", False), + result=result.get("result"), + error=result.get("error", ""), + ) + + logger.debug( + f"Tool '{tool_request.tool_name}' executed: " f"success={response.success}" + ) + + return response.to_dict() + + except Exception as e: + logger.error(f"Error executing tool: {e}", exc_info=True) + return ToolExecutionResponse(success=False, error=str(e)).to_dict() + + @app.get("/tools/list") + async def list_tools() -> dict[str, Any]: + """Get list of available tools and their schemas.""" + schemas = {} + for name, schema in self.tool_dispatcher.schemas.items(): + schemas[name] = { + "name": schema.name, + "description": schema.description, + "parameters": schema.parameters, + "returns": schema.returns, + } + return {"tools": schemas, "count": len(schemas)} + @app.get("/metrics") async def get_metrics(): """Get server performance metrics.""" diff --git a/scenes/crafting_chain.tscn b/scenes/crafting_chain.tscn index 9fc3681..55e1919 100644 --- a/scenes/crafting_chain.tscn +++ b/scenes/crafting_chain.tscn @@ -23,6 +23,9 @@ script = ExtResource("1_crafting") [node name="ToolRegistry" type="ToolRegistry" parent="."] +[node name="IPCClient" type="IPCClient" parent="."] +server_url = "http://127.0.0.1:5000" + [node name="Environment" type="Node3D" parent="."] [node name="Ground" type="MeshInstance3D" parent="Environment"] diff --git a/scenes/foraging.tscn b/scenes/foraging.tscn index 8f9906f..9181e46 100644 --- a/scenes/foraging.tscn +++ b/scenes/foraging.tscn @@ -18,6 +18,9 @@ script = ExtResource("1_foraging") [node name="ToolRegistry" type="ToolRegistry" parent="."] +[node name="IPCClient" type="IPCClient" parent="."] +server_url = "http://127.0.0.1:5000" + [node name="Environment" type="Node3D" parent="."] [node name="Ground" type="MeshInstance3D" parent="Environment"] diff --git a/scenes/team_capture.tscn b/scenes/team_capture.tscn index 4ccfdcc..1c4fd7a 100644 --- a/scenes/team_capture.tscn +++ b/scenes/team_capture.tscn @@ -15,6 +15,9 @@ script = ExtResource("1_team_capture") [node name="ToolRegistry" type="ToolRegistry" parent="."] +[node name="IPCClient" type="IPCClient" parent="."] +server_url = "http://127.0.0.1:5000" + [node name="Environment" type="Node3D" parent="."] [node name="Ground" type="MeshInstance3D" parent="Environment"] diff --git a/scenes/tests/README.md b/scenes/tests/README.md new file mode 100644 index 0000000..519ec9e --- /dev/null +++ b/scenes/tests/README.md @@ -0,0 +1,66 @@ +# Tool Execution Tests + +Test scenes for verifying the tool execution system. + +## Test Scenes + +### test_tool_execution_simple.tscn (RECOMMENDED) +**Direct HTTP test of Python IPC server** + +- **Purpose**: Verify Python server and tool execution works +- **Tests**: HTTP `/tools/execute` endpoint directly +- **Output**: Clear pass/fail for each tool +- **Best for**: Initial verification, debugging + +**How to run:** +1. Start Python server: `START_IPC_SERVER.bat` +2. Open this scene in Godot +3. Press F6 +4. Watch console for results + +### test_tool_execution.tscn +**Full integration test** + +- **Purpose**: Test complete Agent → Python pipeline +- **Tests**: C++ classes, IPC, Python integration +- **Output**: Async responses via signals +- **Best for**: Integration testing, realistic scenarios + +**How to run:** +1. Start Python server: `START_IPC_SERVER.bat` +2. Open this scene in Godot +3. Press F6 +4. Check both consoles for async responses + +## Controls + +Both tests support: +- **T** - Run tests again +- **Q** - Quit + +## Prerequisites + +Python IPC server must be running: +```bash +START_IPC_SERVER.bat +``` + +Or manually: +```bash +cd python +venv\Scripts\activate +python run_ipc_server.py +``` + +## Expected Results + +All 5 tools should pass: +1. `move_to` +2. `pickup_item` +3. `stop_movement` +4. `get_inventory` +5. `navigate_to` + +## Troubleshooting + +See `TESTING_TOOL_EXECUTION.md` in project root for detailed troubleshooting. diff --git a/scenes/tests/test_tool_execution.tscn b/scenes/tests/test_tool_execution.tscn new file mode 100644 index 0000000..c09b87c --- /dev/null +++ b/scenes/tests/test_tool_execution.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=2 format=3 uid="uid://dw8ykp8t3k4hs"] + +[ext_resource type="Script" path="res://scripts/tests/test_tool_execution.gd" id="1_tool_test"] + +[node name="ToolExecutionTest" type="Node"] +script = ExtResource("1_tool_test") + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 5, 10) + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 5, 0) + +[node name="Label" type="Label" parent="."] +offset_left = 20.0 +offset_top = 20.0 +offset_right = 800.0 +offset_bottom = 200.0 +theme_override_font_sizes/font_size = 20 +text = "Tool Execution Test + +Prerequisites: +1. Start Python IPC server: cd python && venv\\Scripts\\activate && python run_ipc_server.py +2. Wait for connection message +3. Tools will execute automatically after 2 seconds + +Controls: +T - Run tests again +Q - Quit + +Check the output console and Python server logs for results." diff --git a/scenes/tests/test_tool_execution_simple.tscn b/scenes/tests/test_tool_execution_simple.tscn new file mode 100644 index 0000000..05c9a61 --- /dev/null +++ b/scenes/tests/test_tool_execution_simple.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=2 format=3 uid="uid://c8ykp8t3k5it"] + +[ext_resource type="Script" path="res://scripts/tests/test_tool_execution_simple.gd" id="1_simple"] + +[node name="SimpleToolTest" type="Node"] +script = ExtResource("1_simple") + +[node name="Label" type="Label" parent="."] +offset_left = 20.0 +offset_top = 20.0 +offset_right = 900.0 +offset_bottom = 250.0 +theme_override_font_sizes/font_size = 18 +text = "Simple Tool Execution Test + +This test directly calls the Python IPC server via HTTP +to verify that tool execution works. + +Prerequisites: +1. Python IPC server must be running + cd python && venv\\Scripts\\activate && python run_ipc_server.py + +2. Server should be on http://127.0.0.1:5000 + +The test will run automatically and show results in the console. + +Controls: +T - Run tests again +Q - Quit + +Check both Godot console and Python server logs for details." diff --git a/scripts/crafting_chain.gd b/scripts/crafting_chain.gd index 84f126c..0acd015 100644 --- a/scripts/crafting_chain.gd +++ b/scripts/crafting_chain.gd @@ -7,6 +7,7 @@ extends Node3D @onready var simulation_manager = $SimulationManager @onready var event_bus = $EventBus @onready var tool_registry = $ToolRegistry +@onready var ipc_client = $IPCClient @onready var agent = $Agents/Agent1 @onready var metrics_label = $UI/MetricsLabel @@ -70,6 +71,14 @@ func _ready(): # Initialize agent agent.agent_id = "crafting_agent_001" + # Connect tool system (IPCClient → ToolRegistry → Agent) + if ipc_client != null and tool_registry != null and agent != null: + tool_registry.set_ipc_client(ipc_client) + agent.set_tool_registry(tool_registry) + print("✓ Tool execution system connected!") + else: + push_warning("Tool execution system not fully available") + # Connect signals simulation_manager.simulation_started.connect(_on_simulation_started) simulation_manager.simulation_stopped.connect(_on_simulation_stopped) diff --git a/scripts/foraging.gd b/scripts/foraging.gd index 8f010d4..0f035d1 100644 --- a/scripts/foraging.gd +++ b/scripts/foraging.gd @@ -7,6 +7,7 @@ extends Node3D @onready var simulation_manager = $SimulationManager @onready var event_bus = $EventBus @onready var tool_registry = $ToolRegistry +@onready var ipc_client = $IPCClient @onready var agent = $Agents/Agent1 @onready var metrics_label = $UI/MetricsLabel @@ -41,6 +42,14 @@ func _ready(): agent.agent_id = "foraging_agent_001" last_position = agent.global_position + # Connect tool system (IPCClient → ToolRegistry → Agent) + if ipc_client != null and tool_registry != null and agent != null: + tool_registry.set_ipc_client(ipc_client) + agent.set_tool_registry(tool_registry) + print("✓ Tool execution system connected!") + else: + push_warning("Tool execution system not fully available") + # Connect simulation signals simulation_manager.simulation_started.connect(_on_simulation_started) simulation_manager.simulation_stopped.connect(_on_simulation_stopped) diff --git a/scripts/team_capture.gd b/scripts/team_capture.gd index 6cceccf..5fe7673 100644 --- a/scripts/team_capture.gd +++ b/scripts/team_capture.gd @@ -7,6 +7,7 @@ extends Node3D @onready var simulation_manager = $SimulationManager @onready var event_bus = $EventBus @onready var tool_registry = $ToolRegistry +@onready var ipc_client = $IPCClient @onready var metrics_label = $UI/MetricsLabel # Scene configuration @@ -43,6 +44,13 @@ func _ready(): push_error("GDExtension nodes not found!") return + # Connect tool system (IPCClient → ToolRegistry → Agents) + if ipc_client != null and tool_registry != null: + tool_registry.set_ipc_client(ipc_client) + print("✓ Tool execution system connected!") + else: + push_warning("Tool execution system not fully available") + # Connect simulation signals simulation_manager.simulation_started.connect(_on_simulation_started) simulation_manager.simulation_stopped.connect(_on_simulation_stopped) @@ -119,6 +127,9 @@ func _initialize_scene(): for child in blue_team_node.get_children(): if child.get_class() == "Agent": child.agent_id = "blue_%s" % child.name + # Connect agent to tool registry + if tool_registry != null: + child.set_tool_registry(tool_registry) blue_team.append({ "agent": child, "id": child.agent_id, @@ -138,6 +149,9 @@ func _initialize_scene(): for child in red_team_node.get_children(): if child.get_class() == "Agent": child.agent_id = "red_%s" % child.name + # Connect agent to tool registry + if tool_registry != null: + child.set_tool_registry(tool_registry) red_team.append({ "agent": child, "id": child.agent_id, diff --git a/scripts/tests/test_tool_execution.gd b/scripts/tests/test_tool_execution.gd new file mode 100644 index 0000000..f778ca5 --- /dev/null +++ b/scripts/tests/test_tool_execution.gd @@ -0,0 +1,145 @@ +extends Node +## Test script for tool execution system +## +## This script tests the complete tool execution pipeline: +## Agent -> ToolRegistry -> IPCClient -> Python IPC Server -> ToolDispatcher -> Tool Functions + +var ipc_client: IPCClient +var tool_registry: ToolRegistry +var agent: Agent + +func _ready(): + print("=== Tool Execution Test ===") + + # Create IPC Client + ipc_client = IPCClient.new() + ipc_client.name = "IPCClient" + ipc_client.server_url = "http://127.0.0.1:5000" + add_child(ipc_client) + + # Create Tool Registry + tool_registry = ToolRegistry.new() + tool_registry.name = "ToolRegistry" + add_child(tool_registry) + + # Connect tool registry to IPC client + tool_registry.set_ipc_client(ipc_client) + + # Register some tools + var move_schema = {} + move_schema["name"] = "move_to" + move_schema["description"] = "Move to a target position" + move_schema["parameters"] = {} + tool_registry.register_tool("move_to", move_schema) + + var pickup_schema = {} + pickup_schema["name"] = "pickup_item" + pickup_schema["description"] = "Pick up an item" + pickup_schema["parameters"] = {} + tool_registry.register_tool("pickup_item", pickup_schema) + + # Create Agent + agent = Agent.new() + agent.name = "TestAgent" + agent.agent_id = "test_agent_001" + add_child(agent) + + # Connect agent to tool registry + agent.set_tool_registry(tool_registry) + + # Connect signals BEFORE attempting connection + ipc_client.response_received.connect(_on_response_received) + ipc_client.connection_failed.connect(_on_connection_failed) + + # Connect to server + print("Connecting to IPC server...") + print("IMPORTANT: Make sure Python IPC server is running!") + print(" cd python && venv\\Scripts\\activate && python run_ipc_server.py") + + ipc_client.connect_to_server("http://127.0.0.1:5000") + + # Wait a moment for connection, then test tools + print("Waiting 3 seconds for connection...") + await get_tree().create_timer(3.0).timeout + + if not ipc_client.is_server_connected(): + print("\n[WARNING] Not connected to server!") + print("Please check that:") + print("1. Python IPC server is running") + print("2. Server is on http://127.0.0.1:5000") + print("3. No firewall is blocking the connection") + print("\nTrying to test tools anyway...") + + test_tools() + +func test_tools(): + print("\n=== Testing Tool Execution ===") + print("Note: Tool execution is async - responses come via signals") + print("Check the IPC Response Received section below for actual results\n") + + # Test 1: Move tool + print("[Test 1] Testing move_to tool...") + var move_params = { + "target_position": [10.0, 0.0, 5.0], + "speed": 1.5 + } + var move_result = agent.call_tool("move_to", move_params) + print("Request sent: ", move_result) + await get_tree().create_timer(0.5).timeout + + # Test 2: Pickup item tool + print("\n[Test 2] Testing pickup_item tool...") + var pickup_params = { + "item_id": "sword_001" + } + var pickup_result = agent.call_tool("pickup_item", pickup_params) + print("Request sent: ", pickup_result) + await get_tree().create_timer(0.5).timeout + + # Test 3: Stop movement tool + print("\n[Test 3] Testing stop_movement tool...") + var stop_result = agent.call_tool("stop_movement", {}) + print("Request sent: ", stop_result) + await get_tree().create_timer(0.5).timeout + + # Test 4: Get inventory tool + print("\n[Test 4] Testing get_inventory tool...") + var inventory_result = agent.call_tool("get_inventory", {}) + print("Request sent: ", inventory_result) + await get_tree().create_timer(0.5).timeout + + # Test 5: Direct ToolRegistry execution + print("\n[Test 5] Testing navigate_to tool...") + var direct_result = tool_registry.execute_tool("navigate_to", { + "target_position": [20.0, 0.0, 10.0] + }) + print("Request sent: ", direct_result) + + print("\n=== All Tool Requests Sent ===") + print("Waiting for responses (check 'IPC Response Received' below)...") + print("Python server log should show tool executions") + + # Wait a bit for all responses + await get_tree().create_timer(2.0).timeout + print("\n=== Test Complete ===") + print("If you saw response_received callbacks above, tool execution works!") + +func _on_response_received(response: Dictionary): + print("\n[IPC Response Received]") + print("Response: ", response) + +func _on_connection_failed(error: String): + print("\n[IPC Connection Failed]") + print("Error: ", error) + print("Make sure Python IPC server is running:") + print(" cd python") + print(" venv\\Scripts\\activate") + print(" python run_ipc_server.py") + +func _input(event): + if event is InputEventKey and event.pressed: + if event.keycode == KEY_T: + test_tools() + elif event.keycode == KEY_Q: + print("Quitting...") + get_tree().quit() diff --git a/scripts/tests/test_tool_execution.gd.uid b/scripts/tests/test_tool_execution.gd.uid new file mode 100644 index 0000000..e71c65e --- /dev/null +++ b/scripts/tests/test_tool_execution.gd.uid @@ -0,0 +1 @@ +uid://dirlg4f8vrps diff --git a/scripts/tests/test_tool_execution_simple.gd b/scripts/tests/test_tool_execution_simple.gd new file mode 100644 index 0000000..d844912 --- /dev/null +++ b/scripts/tests/test_tool_execution_simple.gd @@ -0,0 +1,133 @@ +extends Node +## Simple HTTP-based tool execution test +## Tests the Python IPC server's /tools/execute endpoint directly + +var test_count = 0 +var tests_passed = 0 + +func _ready(): + print("=== Simple Tool Execution Test ===") + print("This test calls the Python IPC server directly using HTTP\n") + + # Wait a moment then start tests + await get_tree().create_timer(1.0).timeout + run_tests() + +func run_tests(): + print("Starting tool execution tests...") + print("Make sure Python IPC server is running on http://127.0.0.1:5000\n") + + # Test 1: move_to + await execute_tool_test("move_to", { + "target_position": [10.0, 0.0, 5.0], + "speed": 1.5 + }) + + # Test 2: pickup_item + await execute_tool_test("pickup_item", { + "item_id": "sword_001" + }) + + # Test 3: stop_movement + await execute_tool_test("stop_movement", {}) + + # Test 4: get_inventory + await execute_tool_test("get_inventory", {}) + + # Test 5: navigate_to + await execute_tool_test("navigate_to", { + "target_position": [20.0, 0.0, 10.0] + }) + + print("\n==================================================") + print("Test Results: %d/%d passed" % [tests_passed, test_count]) + print("==================================================") + + if tests_passed == test_count: + print("\n✓ All tests PASSED! Tool execution system is working!") + else: + print("\n✗ Some tests failed. Check Python server logs.") + +func execute_tool_test(tool_name: String, params: Dictionary): + test_count += 1 + print("\n[Test %d] Executing tool: %s" % [test_count, tool_name]) + print(" Parameters: %s" % [params]) + + # Create a new HTTPRequest for this test + var http_request = HTTPRequest.new() + add_child(http_request) + + # Build request JSON + var request_body = { + "tool_name": tool_name, + "params": params, + "agent_id": "test_agent", + "tick": 0 + } + + var json = JSON.stringify(request_body) + var headers = ["Content-Type: application/json"] + + # Send request + var error = http_request.request( + "http://127.0.0.1:5000/tools/execute", + headers, + HTTPClient.METHOD_POST, + json + ) + + if error != OK: + print(" ✗ FAILED: HTTP request error: %d" % error) + http_request.queue_free() + return + + # Wait for response + var response = await http_request.request_completed + + var result = response[0] # HTTPRequest result + var response_code = response[1] # HTTP status code + var response_headers = response[2] + var body = response[3] + + if result != HTTPRequest.RESULT_SUCCESS: + print(" ✗ FAILED: Request failed with code: %d" % result) + http_request.queue_free() + return + + if response_code != 200: + print(" ✗ FAILED: HTTP %d" % response_code) + http_request.queue_free() + return + + # Parse JSON response + var body_string = body.get_string_from_utf8() + var json_parser = JSON.new() + var parse_error = json_parser.parse(body_string) + + if parse_error != OK: + print(" ✗ FAILED: JSON parse error") + print(" Response body: %s" % body_string) + http_request.queue_free() + return + + var response_data = json_parser.get_data() + + # Check if tool executed successfully + if response_data.has("success") and response_data["success"]: + print(" ✓ PASSED") + print(" Result: %s" % [response_data.get("result", "none")]) + tests_passed += 1 + else: + print(" ✗ FAILED: %s" % [response_data.get("error", "Unknown error")]) + + # Cleanup + http_request.queue_free() + +func _input(event): + if event is InputEventKey and event.pressed: + if event.keycode == KEY_T: + tests_passed = 0 + test_count = 0 + run_tests() + elif event.keycode == KEY_Q: + get_tree().quit() diff --git a/scripts/tests/test_tool_execution_simple.gd.uid b/scripts/tests/test_tool_execution_simple.gd.uid new file mode 100644 index 0000000..9c1e1fb --- /dev/null +++ b/scripts/tests/test_tool_execution_simple.gd.uid @@ -0,0 +1 @@ +uid://csof2q700plsb