From 311ce0d589e600e61ef29caa32ad60ffe7a2fa1e Mon Sep 17 00:00:00 2001 From: justinmadison Date: Fri, 7 Nov 2025 08:50:44 -0800 Subject: [PATCH 1/2] fix lint formating --- .claude/settings.local.json | 6 ++++- python/agent_runtime/agent.py | 30 ++++++++++++------------- python/agent_runtime/runtime.py | 16 ++++++------- python/agent_runtime/tool_dispatcher.py | 25 +++++++++++---------- python/backends/base.py | 12 +++++----- python/backends/llama_cpp_backend.py | 14 ++++++------ python/tools/__init__.py | 4 ++-- python/tools/inventory.py | 12 +++++----- python/tools/movement.py | 10 ++++----- python/tools/world_query.py | 24 ++++++++++---------- tests/test_agent.py | 2 +- tests/test_tool_dispatcher.py | 2 +- 12 files changed, 81 insertions(+), 76 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 51913d2..1f8b625 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,11 @@ "Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)", - "Bash(gh pr create:*)" + "Bash(gh pr create:*)", + "Bash(if [ -d .vscode ])", + "Bash(then echo \"exists\")", + "Bash(else echo \"not exists\")", + "Bash(fi)" ], "deny": [], "ask": [] diff --git a/python/agent_runtime/agent.py b/python/agent_runtime/agent.py index 55a3ff4..b702559 100644 --- a/python/agent_runtime/agent.py +++ b/python/agent_runtime/agent.py @@ -2,10 +2,10 @@ Core Agent implementation with perception, reasoning, and action capabilities. """ -from typing import Any, Dict, List, Optional +import logging from dataclasses import dataclass, field from datetime import datetime -import logging +from typing import Any logger = logging.getLogger(__name__) @@ -15,7 +15,7 @@ class Observation: """Represents an agent's observation of the world.""" timestamp: datetime - data: Dict[str, Any] + data: dict[str, Any] source: str = "world" @@ -24,8 +24,8 @@ class Action: """Represents an action the agent wants to take.""" tool_name: str - parameters: Dict[str, Any] - reasoning: Optional[str] = None + parameters: dict[str, Any] + reasoning: str | None = None @dataclass @@ -33,10 +33,10 @@ class AgentState: """Internal state of an agent.""" agent_id: str - goals: List[str] = field(default_factory=list) - observations: List[Observation] = field(default_factory=list) - action_history: List[Action] = field(default_factory=list) - metadata: Dict[str, Any] = field(default_factory=dict) + goals: list[str] = field(default_factory=list) + observations: list[Observation] = field(default_factory=list) + action_history: list[Action] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) class Agent: @@ -50,9 +50,9 @@ class Agent: def __init__( self, agent_id: str, - backend: Optional[Any] = None, # LLM backend instance - tools: Optional[List[str]] = None, - goals: Optional[List[str]] = None, + backend: Any | None = None, # LLM backend instance + tools: list[str] | None = None, + goals: list[str] | None = None, ): """ Initialize an agent. @@ -73,7 +73,7 @@ def __init__( logger.info(f"Initialized agent {agent_id} with {len(self.available_tools)} tools") - def perceive(self, observation: Dict[str, Any], source: str = "world") -> None: + def perceive(self, observation: dict[str, Any], source: str = "world") -> None: """ Process a new observation from the environment. @@ -94,7 +94,7 @@ def perceive(self, observation: Dict[str, Any], source: str = "world") -> None: logger.debug(f"Agent {self.state.agent_id} received observation from {source}") - def decide_action(self) -> Optional[Action]: + def decide_action(self) -> Action | None: """ Use the LLM backend to decide the next action based on current state. @@ -162,7 +162,7 @@ def _query_llm(self, context: str) -> str: # TODO: Implement actual LLM querying based on backend type return '{"tool": "idle", "params": {}}' - def _parse_action(self, response: str) -> Optional[Action]: + def _parse_action(self, response: str) -> Action | None: """ Parse the LLM response into an Action object. diff --git a/python/agent_runtime/runtime.py b/python/agent_runtime/runtime.py index 7a8f2df..be5def4 100644 --- a/python/agent_runtime/runtime.py +++ b/python/agent_runtime/runtime.py @@ -2,12 +2,12 @@ Agent Runtime - orchestrates multiple agents and their interactions with the environment. """ -from typing import Any, Dict, List, Optional import asyncio import logging from concurrent.futures import ThreadPoolExecutor +from typing import Any -from .agent import Agent, Action +from .agent import Action, Agent logger = logging.getLogger(__name__) @@ -22,7 +22,7 @@ class AgentRuntime: def __init__( self, max_workers: int = 4, - backend_config: Optional[Dict[str, Any]] = None, + backend_config: dict[str, Any] | None = None, ): """ Initialize the agent runtime. @@ -31,7 +31,7 @@ def __init__( max_workers: Maximum number of concurrent agent workers backend_config: Configuration for LLM backend """ - self.agents: Dict[str, Agent] = {} + self.agents: dict[str, Agent] = {} self.executor = ThreadPoolExecutor(max_workers=max_workers) self.backend_config = backend_config or {} self.running = False @@ -63,7 +63,7 @@ def unregister_agent(self, agent_id: str) -> None: del self.agents[agent_id] logger.info(f"Unregistered agent {agent_id}") - async def process_tick(self, tick: int, observations: Dict[str, Any]) -> Dict[str, Action]: + async def process_tick(self, tick: int, observations: dict[str, Any]) -> dict[str, Action]: """ Process a single simulation tick for all agents. @@ -99,7 +99,7 @@ async def process_tick(self, tick: int, observations: Dict[str, Any]) -> Dict[st return actions - async def _agent_decide(self, agent: Agent) -> Optional[Action]: + async def _agent_decide(self, agent: Agent) -> Action | None: """ Execute agent decision-making asynchronously. @@ -123,11 +123,11 @@ def stop(self) -> None: self.executor.shutdown(wait=True) logger.info("AgentRuntime stopped") - def get_agent(self, agent_id: str) -> Optional[Agent]: + def get_agent(self, agent_id: str) -> Agent | None: """Get an agent by ID.""" return self.agents.get(agent_id) - def get_all_agents(self) -> List[Agent]: + def get_all_agents(self) -> list[Agent]: """Get all registered agents.""" return list(self.agents.values()) diff --git a/python/agent_runtime/tool_dispatcher.py b/python/agent_runtime/tool_dispatcher.py index a3788c9..ed32b27 100644 --- a/python/agent_runtime/tool_dispatcher.py +++ b/python/agent_runtime/tool_dispatcher.py @@ -2,10 +2,11 @@ Tool Dispatcher - manages tool registration and execution for agents. """ -from typing import Any, Callable, Dict, Optional -from dataclasses import dataclass import json import logging +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any logger = logging.getLogger(__name__) @@ -16,8 +17,8 @@ class ToolSchema: name: str description: str - parameters: Dict[str, Any] # JSON schema for parameters - returns: Dict[str, Any] # JSON schema for return value + parameters: dict[str, Any] # JSON schema for parameters + returns: dict[str, Any] # JSON schema for return value class ToolDispatcher: @@ -30,8 +31,8 @@ class ToolDispatcher: def __init__(self): """Initialize the tool dispatcher.""" - self.tools: Dict[str, Callable] = {} - self.schemas: Dict[str, ToolSchema] = {} + self.tools: dict[str, Callable] = {} + self.schemas: dict[str, ToolSchema] = {} logger.info("Initialized ToolDispatcher") @@ -40,8 +41,8 @@ def register_tool( name: str, function: Callable, description: str, - parameters: Dict[str, Any], - returns: Dict[str, Any], + parameters: dict[str, Any], + returns: dict[str, Any], ) -> None: """ Register a tool with the dispatcher. @@ -72,7 +73,7 @@ def unregister_tool(self, name: str) -> None: del self.schemas[name] logger.info(f"Unregistered tool: {name}") - def execute_tool(self, name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + def execute_tool(self, name: str, parameters: dict[str, Any]) -> dict[str, Any]: """ Execute a tool with given parameters. @@ -114,7 +115,7 @@ def execute_tool(self, name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: "error": str(e), } - def _validate_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> bool: + def _validate_parameters(self, tool_name: str, parameters: dict[str, Any]) -> bool: """ Validate parameters against tool schema. @@ -138,11 +139,11 @@ def _validate_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> bo # TODO: More thorough JSON schema validation return True - def get_tool_schema(self, name: str) -> Optional[ToolSchema]: + def get_tool_schema(self, name: str) -> ToolSchema | None: """Get the schema for a tool.""" return self.schemas.get(name) - def get_all_schemas(self) -> Dict[str, ToolSchema]: + def get_all_schemas(self) -> dict[str, ToolSchema]: """Get all tool schemas.""" return self.schemas.copy() diff --git a/python/backends/base.py b/python/backends/base.py index d2b3622..8439f7e 100644 --- a/python/backends/base.py +++ b/python/backends/base.py @@ -3,8 +3,8 @@ """ from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional from dataclasses import dataclass +from typing import Any @dataclass @@ -25,7 +25,7 @@ class GenerationResult: text: str tokens_used: int finish_reason: str # "stop", "length", "error" - metadata: Dict[str, Any] + metadata: dict[str, Any] class BaseBackend(ABC): @@ -48,8 +48,8 @@ def __init__(self, config: BackendConfig): def generate( self, prompt: str, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, + temperature: float | None = None, + max_tokens: int | None = None, ) -> GenerationResult: """ Generate text from prompt. @@ -68,8 +68,8 @@ def generate( def generate_with_tools( self, prompt: str, - tools: List[Dict[str, Any]], - temperature: Optional[float] = None, + tools: list[dict[str, Any]], + temperature: float | None = None, ) -> GenerationResult: """ Generate with function/tool calling support. diff --git a/python/backends/llama_cpp_backend.py b/python/backends/llama_cpp_backend.py index 3cf3b47..2a9dc70 100644 --- a/python/backends/llama_cpp_backend.py +++ b/python/backends/llama_cpp_backend.py @@ -2,11 +2,11 @@ llama.cpp backend adapter. """ -from typing import Any, Dict, List, Optional -import logging import json +import logging +from typing import Any -from .base import BaseBackend, BackendConfig, GenerationResult +from .base import BackendConfig, BaseBackend, GenerationResult logger = logging.getLogger(__name__) @@ -48,8 +48,8 @@ def _load_model(self) -> None: def generate( self, prompt: str, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, + temperature: float | None = None, + max_tokens: int | None = None, ) -> GenerationResult: """Generate text from prompt.""" if not self.llm: @@ -90,8 +90,8 @@ def generate( def generate_with_tools( self, prompt: str, - tools: List[Dict[str, Any]], - temperature: Optional[float] = None, + tools: list[dict[str, Any]], + temperature: float | None = None, ) -> GenerationResult: """Generate with function calling support.""" if not self.llm: diff --git a/python/tools/__init__.py b/python/tools/__init__.py index ef37737..663a051 100644 --- a/python/tools/__init__.py +++ b/python/tools/__init__.py @@ -4,9 +4,9 @@ Standard tools for agent world interaction. """ -from .world_query import register_world_query_tools -from .movement import register_movement_tools from .inventory import register_inventory_tools +from .movement import register_movement_tools +from .world_query import register_world_query_tools __all__ = [ "register_world_query_tools", diff --git a/python/tools/inventory.py b/python/tools/inventory.py index 5725b13..f99ad40 100644 --- a/python/tools/inventory.py +++ b/python/tools/inventory.py @@ -2,13 +2,13 @@ Inventory and item interaction tools. """ -from typing import Any, Dict, List, Optional import logging +from typing import Any logger = logging.getLogger(__name__) -def pickup_item(item_id: str) -> Dict[str, Any]: +def pickup_item(item_id: str) -> dict[str, Any]: """ Pick up an item from the world. @@ -26,7 +26,7 @@ def pickup_item(item_id: str) -> Dict[str, Any]: } -def drop_item(item_id: str) -> Dict[str, bool]: +def drop_item(item_id: str) -> dict[str, bool]: """ Drop an item from inventory. @@ -41,7 +41,7 @@ def drop_item(item_id: str) -> Dict[str, bool]: return {"success": True} -def use_item(item_id: str, target: Optional[str] = None) -> Dict[str, Any]: +def use_item(item_id: str, target: str | None = None) -> dict[str, Any]: """ Use an item from inventory. @@ -60,7 +60,7 @@ def use_item(item_id: str, target: Optional[str] = None) -> Dict[str, Any]: } -def get_inventory() -> List[Dict[str, Any]]: +def get_inventory() -> list[dict[str, Any]]: """ Get current inventory contents. @@ -72,7 +72,7 @@ def get_inventory() -> List[Dict[str, Any]]: return [] -def craft_item(recipe: str, ingredients: List[str]) -> Dict[str, Any]: +def craft_item(recipe: str, ingredients: list[str]) -> dict[str, Any]: """ Craft an item using ingredients. diff --git a/python/tools/movement.py b/python/tools/movement.py index 720111a..1e06468 100644 --- a/python/tools/movement.py +++ b/python/tools/movement.py @@ -2,13 +2,13 @@ Movement and navigation tools. """ -from typing import Any, Dict, List import logging +from typing import Any logger = logging.getLogger(__name__) -def move_to(target_position: List[float], speed: float = 1.0) -> Dict[str, Any]: +def move_to(target_position: list[float], speed: float = 1.0) -> dict[str, Any]: """ Move agent to target position. @@ -29,7 +29,7 @@ def move_to(target_position: List[float], speed: float = 1.0) -> Dict[str, Any]: } -def navigate_to(target_position: List[float]) -> Dict[str, Any]: +def navigate_to(target_position: list[float]) -> dict[str, Any]: """ Navigate to target using pathfinding. @@ -49,7 +49,7 @@ def navigate_to(target_position: List[float]) -> Dict[str, Any]: } -def stop_movement() -> Dict[str, bool]: +def stop_movement() -> dict[str, bool]: """ Stop all current movement. @@ -61,7 +61,7 @@ def stop_movement() -> Dict[str, bool]: return {"success": True} -def rotate_to_face(target_position: List[float]) -> Dict[str, Any]: +def rotate_to_face(target_position: list[float]) -> dict[str, Any]: """ Rotate agent to face target position. diff --git a/python/tools/world_query.py b/python/tools/world_query.py index e7e0048..20d06ba 100644 --- a/python/tools/world_query.py +++ b/python/tools/world_query.py @@ -2,17 +2,17 @@ World query tools - vision rays, entity detection, distance calculations. """ -from typing import Any, Dict, List, Optional import logging +from typing import Any logger = logging.getLogger(__name__) def raycast( - origin: List[float], - direction: List[float], + origin: list[float], + direction: list[float], max_distance: float = 100.0, -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Cast a ray from origin in direction to detect objects. @@ -37,10 +37,10 @@ def raycast( def get_nearby_entities( - position: List[float], + position: list[float], radius: float, - entity_type: Optional[str] = None, -) -> List[Dict[str, Any]]: + entity_type: str | None = None, +) -> list[dict[str, Any]]: """ Get all entities within radius of position. @@ -59,11 +59,11 @@ def get_nearby_entities( def get_visible_entities( - agent_position: List[float], - agent_forward: List[float], + agent_position: list[float], + agent_forward: list[float], fov_degrees: float = 90.0, max_distance: float = 50.0, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """ Get entities visible to the agent based on FOV cone. @@ -83,8 +83,8 @@ def get_visible_entities( def measure_distance( - point_a: List[float], - point_b: List[float], + point_a: list[float], + point_b: list[float], ) -> float: """ Calculate Euclidean distance between two points. diff --git a/tests/test_agent.py b/tests/test_agent.py index 4061267..fbc677a 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -3,7 +3,7 @@ """ import pytest -from agent_runtime.agent import Agent, Observation, Action, AgentState +from agent_runtime.agent import Agent, AgentState def test_agent_initialization(): diff --git a/tests/test_tool_dispatcher.py b/tests/test_tool_dispatcher.py index 2400b89..868260b 100644 --- a/tests/test_tool_dispatcher.py +++ b/tests/test_tool_dispatcher.py @@ -3,7 +3,7 @@ """ import pytest -from agent_runtime.tool_dispatcher import ToolDispatcher, ToolSchema +from agent_runtime.tool_dispatcher import ToolDispatcher def dummy_tool(x: int, y: int) -> int: From 677e0ed50201a5412444f23e9a0a1fa517362729 Mon Sep 17 00:00:00 2001 From: justinmadison Date: Fri, 7 Nov 2025 11:49:56 -0800 Subject: [PATCH 2/2] fixes to godot scripts, change to python 1.1 to make instalation easier --- .claude/project-context.md | 39 ++++- .claude/settings.local.json | 11 +- PYTHON_SETUP.md | 138 +++++++++++++++ TROUBLESHOOTING_IPC.md | 140 +++++++++++++++ docs/ipc.md | 296 ++++++++++++++++++++++++++++++++ godot/include/agent_arena.h | 45 +++++ godot/src/agent_arena.cpp | 157 +++++++++++++++++ godot/src/register_types.cpp | 1 + python/.gdignore | 2 + python/ipc/__init__.py | 8 + python/ipc/messages.py | 151 ++++++++++++++++ python/ipc/server.py | 249 +++++++++++++++++++++++++++ python/requirements-minimal.txt | 14 ++ python/requirements.txt | 4 + python/run_ipc_server.py | 83 +++++++++ scenes/test_IPC.tscn | 3 + scenes/test_arena.gd | 99 ----------- scenes/test_arena.gd.uid | 1 - scenes/test_arena.tscn | 4 +- 19 files changed, 1334 insertions(+), 111 deletions(-) create mode 100644 PYTHON_SETUP.md create mode 100644 TROUBLESHOOTING_IPC.md create mode 100644 docs/ipc.md create mode 100644 python/.gdignore create mode 100644 python/ipc/__init__.py create mode 100644 python/ipc/messages.py create mode 100644 python/ipc/server.py create mode 100644 python/requirements-minimal.txt create mode 100644 python/run_ipc_server.py create mode 100644 scenes/test_IPC.tscn delete mode 100644 scenes/test_arena.gd delete mode 100644 scenes/test_arena.gd.uid diff --git a/.claude/project-context.md b/.claude/project-context.md index e2b9de1..e15a0a6 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -11,7 +11,7 @@ Quick reference for Claude Code sessions. ## Tech Stack - **Godot 4.5**: C++ GDExtension module for simulation -- **Python 3.11+**: Agent runtime, LLM backends, tools +- **Python 3.11**: Agent runtime, LLM backends, tools (3.11 required - many ML packages don't support 3.14 yet) - **Visual Studio 2022**: C++ compilation (MSVC) - **CMake 3.20+**: Build system - **License**: Apache 2.0 @@ -36,8 +36,10 @@ c:\Projects\Agent Arena\ │ ├── memory/ # Memory systems (RAG, episodes) │ └── evals/ # Evaluation harness ├── configs/ # Hydra configs -├── scenes/ # Godot benchmark scenes -│ └── test_arena.tscn # Test scene with SimulationManager & Agent +├── scenes/ # Godot benchmark scenes (.tscn files) +├── scripts/ # GDScript files +│ ├── tests/ # Test scripts (test_extension.gd, ipc_test.gd) +│ └── test_arena.gd # Main test arena script ├── tests/ # Python unit tests └── docs/ # Documentation ``` @@ -58,9 +60,10 @@ c:\Projects\Agent Arena\ - ✅ Extension tested and working in Godot 4.5.1 - ✅ Test scene created with working controls - ✅ Core classes verified: SimulationManager, Agent, EventBus, ToolRegistry -- ⏳ Next: Implement IPC between Godot and Python +- ✅ IPC system implemented (Godot ↔ Python via HTTP/FastAPI) - ⏳ Next: Create actual benchmark scenes (foraging, crafting_chain, team_capture) - ⏳ Next: Set up Python environment and agent runtime +- ⏳ Next: Integrate LLM backends with agent decision-making ## Development Commands @@ -101,6 +104,20 @@ cd tests pytest -v ``` +### Run IPC Server (Godot ↔ Python Communication) +```bash +# Start Python IPC server +cd python +venv\Scripts\activate +python run_ipc_server.py + +# With custom options +python run_ipc_server.py --host 127.0.0.1 --port 5000 --workers 4 --debug + +# Test IPC in Godot +# Open scenes/ipc_test.gd in Godot editor and run it +``` + ## Common Tasks ### Adding a New Tool @@ -146,11 +163,17 @@ pytest -v - Tool management system for agent actions - Methods: `register_tool()`, `unregister_tool()`, `get_tool_schema()`, `execute_tool()` +### IPCClient (Node) +- HTTP client for Godot ↔ Python communication +- Methods: `connect_to_server()`, `send_tick_request()`, `get_tick_response()`, `has_response()` +- Properties: `server_url` +- Signals: `response_received`, `connection_failed` + ## Known Issues -- IPC layer not yet implemented (Godot ↔ Python communication) -- Benchmark scenes are empty placeholders -- Python environment not yet set up -- Tool execution currently returns stub responses +- 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 ## References - Godot docs: https://docs.godotengine.org/ diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1f8b625..a53acf6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,16 @@ "Bash(if [ -d .vscode ])", "Bash(then echo \"exists\")", "Bash(else echo \"not exists\")", - "Bash(fi)" + "Bash(fi)", + "Bash(py --list:*)", + "Bash(if exist \"c:\\Projects\\Agent Arena\\.godot\" echo \".godot folder exists\" else echo \".godot folder not found\")", + "Bash(test:*)", + "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe\" --build . --config Debug --target agent_arena)", + "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe\" --build . --config Debug --clean-first)", + "Bash(del /F /Q agent_arena.*)", + "Bash(del /F /Q CMakeFilesagent_arena.dir* 2)", + "Bash(nul)", + "Bash(ls -la \"C:\\Projects\\Agent Arena\\bin\\windows\"\" 2>nul || echo \"Directory does not exist \")" ], "deny": [], "ask": [] diff --git a/PYTHON_SETUP.md b/PYTHON_SETUP.md new file mode 100644 index 0000000..1b68c9c --- /dev/null +++ b/PYTHON_SETUP.md @@ -0,0 +1,138 @@ +# Python Environment Setup + +## Python Version Requirement + +**Agent Arena requires Python 3.11** (specifically tested with 3.11.9) + +Many ML/AI packages (llama-cpp-python, torch, faiss, etc.) don't yet have pre-built wheels for Python 3.14. Using Python 3.11 ensures all dependencies install smoothly. + +## Installation Steps + +### 1. Install Python 3.11 + +**Download**: [Python 3.11.9 (64-bit)](https://www.python.org/downloads/release/python-3119/) + +**During installation**: +- ✅ Check "Add Python 3.11 to PATH" +- ✅ Check "Install for all users" (optional) + +**Verify installation**: +```bash +py -3.11 --version +# Should output: Python 3.11.9 +``` + +### 2. Create Virtual Environment + +```bash +cd "c:\Projects\Agent Arena\python" + +# Create venv with Python 3.11 +py -3.11 -m venv venv + +# Activate venv +venv\Scripts\activate + +# Verify Python version in venv +python --version +# Should output: Python 3.11.9 +``` + +### 3. Install Dependencies + +#### Option A: Full Installation (Recommended) +Installs all dependencies including LLM backends, vector stores, etc. + +```bash +pip install --upgrade pip +pip install -r requirements.txt +``` + +**Note**: This may take 5-10 minutes as some packages (torch, transformers) are large. + +#### Option B: Minimal Installation (For IPC Testing Only) +If you just want to test the IPC system quickly: + +```bash +pip install --upgrade pip +pip install -r requirements-minimal.txt +``` + +This installs only FastAPI, uvicorn, and essential packages. You'll need the full installation later for LLM functionality. + +### 4. Verify Installation + +```bash +# Test imports +python -c "import fastapi; import uvicorn; print('IPC dependencies OK')" + +# Full test (only if you did full installation) +python -c "import torch; import transformers; import faiss; print('All dependencies OK')" +``` + +## Running the IPC Server + +```bash +cd "c:\Projects\Agent Arena\python" +venv\Scripts\activate +python run_ipc_server.py +``` + +You should see: +``` +============================================================ +Agent Arena IPC Server +============================================================ +Host: 127.0.0.1 +Port: 5000 +Max Workers: 4 +============================================================ +INFO: Started server process +INFO: Uvicorn running on http://127.0.0.1:5000 +``` + +## Troubleshooting + +### "Python 3.11 not found" +- Make sure you installed Python 3.11 from python.org +- Try `py --list` to see available Python versions +- If 3.11 doesn't appear, reinstall and check "Add to PATH" + +### "No module named 'fastapi'" +- Make sure venv is activated: `venv\Scripts\activate` +- Reinstall: `pip install -r requirements.txt` + +### "ERROR: Could not build wheels for llama-cpp-python" +- Requires Visual Studio C++ Build Tools +- On Windows, install: https://visualstudio.microsoft.com/visual-cpp-build-tools/ +- Select "Desktop development with C++" workload + +### "torch" installation is very slow +- torch is ~2GB, be patient +- Alternative: Use CPU-only version: `pip install torch --index-url https://download.pytorch.org/whl/cpu` + +### Port 5000 already in use +- Change port: `python run_ipc_server.py --port 5001` +- Update Godot IPCClient.server_url to match + +## Using Different Python Versions + +If you need to keep Python 3.14 as default but use 3.11 for this project: + +```bash +# Always use py -3.11 to create the venv +py -3.11 -m venv venv + +# Once activated, the venv uses 3.11 automatically +venv\Scripts\activate +python --version # Shows 3.11.9 +``` + +## Next Steps + +After setup: +1. Start IPC server: `python run_ipc_server.py` +2. Open Godot and run [scenes/ipc_test.gd](scenes/ipc_test.gd) +3. Verify communication in console logs + +See [docs/ipc.md](docs/ipc.md) for detailed IPC documentation. diff --git a/TROUBLESHOOTING_IPC.md b/TROUBLESHOOTING_IPC.md new file mode 100644 index 0000000..7dec151 --- /dev/null +++ b/TROUBLESHOOTING_IPC.md @@ -0,0 +1,140 @@ +# 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/docs/ipc.md b/docs/ipc.md new file mode 100644 index 0000000..78c3462 --- /dev/null +++ b/docs/ipc.md @@ -0,0 +1,296 @@ +# IPC System - Godot ↔ Python Communication + +## Overview + +The IPC (Inter-Process Communication) system enables real-time communication between the Godot simulation engine (C++) and the Python agent runtime (LLM-based decision making). + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Godot Simulation (C++) │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Simulation │──────▶│ IPCClient │──┐ │ +│ │ Manager │ │ (Node) │ │ │ +│ └──────────────┘ └──────────────┘ │ │ +│ │ │ │ +│ │ │ HTTP/JSON │ +│ ┌──────▼──────┐ │ │ +│ │ Agents │ │ │ +│ │ (Nodes) │ │ │ +│ └─────────────┘ │ │ +└───────────────────────────────────────────┼─────────────────┘ + │ + │ POST /tick + │ +┌───────────────────────────────────────────┼─────────────────┐ +│ Python Runtime │ │ +│ ┌─────────────────────────────────────────▼──────────────┐ │ +│ │ FastAPI IPC Server (IPCServer) │ │ +│ └─────────────────────────────────────────┬──────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────▼─────────────┐ │ +│ │ AgentRuntime │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Agent 1 │ │ Agent 2 │ │ Agent N │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └──────────────────────────────────────────┬─────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────▼─────────────┐ │ +│ │ LLM Backend (llama.cpp, etc) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Components + +### Godot Side (C++) + +#### IPCClient (Node) +- **Purpose**: HTTP client for sending perception data to Python and receiving action decisions +- **Key Methods**: + - `connect_to_server(url)` - Connect to Python IPC server + - `send_tick_request(tick, perceptions)` - Send perception data for a simulation tick + - `get_tick_response()` - Retrieve action decisions from Python + - `has_response()` - Check if response is available +- **Signals**: + - `response_received(response)` - Emitted when actions are received + - `connection_failed(error)` - Emitted on connection errors + +### Python Side + +#### IPCServer (FastAPI) +- **Purpose**: HTTP server that receives perception data and returns action decisions +- **Endpoints**: + - `GET /` - Server status and metrics + - `GET /health` - Health check + - `POST /tick` - Process simulation tick (main endpoint) + - `POST /agents/register` - Register new agents + - `GET /metrics` - Performance metrics +- **Key Methods**: + - `run()` - Start server (blocking) + - `run_async()` - Start server (async) + +#### Message Schemas + +##### PerceptionMessage +Observation data sent from Godot to Python for a single agent: +```python +{ + "agent_id": "agent_001", + "tick": 1234, + "position": [x, y, z], + "rotation": [x, y, z], + "velocity": [x, y, z], + "visible_entities": [...], + "inventory": [...], + "health": 100.0, + "energy": 100.0, + "custom_data": {} +} +``` + +##### ActionMessage +Action decision sent from Python to Godot for a single agent: +```python +{ + "agent_id": "agent_001", + "tick": 1234, + "tool": "move_to", + "params": { + "target_position": [x, y, z], + "speed": 1.5 + }, + "reasoning": "Moving towards resource location" +} +``` + +##### TickRequest +Full request sent from Godot containing all agent perceptions: +```python +{ + "tick": 1234, + "perceptions": [PerceptionMessage, ...], + "simulation_state": {} +} +``` + +##### TickResponse +Full response sent from Python containing all agent actions: +```python +{ + "tick": 1234, + "actions": [ActionMessage, ...], + "metrics": { + "tick_time_ms": 150.5, + "agents_processed": 10, + "actions_generated": 8 + } +} +``` + +## Usage + +### 1. Start Python IPC Server + +```bash +cd python +python -m venv venv +venv\Scripts\activate # Windows +source venv/bin/activate # Linux/Mac +pip install -r requirements.txt + +# Start server +python run_ipc_server.py + +# With custom options +python run_ipc_server.py --host 127.0.0.1 --port 5000 --workers 4 --debug +``` + +### 2. Use in Godot Scene + +```gdscript +extends Node + +var simulation_manager: SimulationManager +var ipc_client: IPCClient + +func _ready(): + # Create simulation manager + simulation_manager = SimulationManager.new() + add_child(simulation_manager) + + # Create IPC client + ipc_client = IPCClient.new() + ipc_client.server_url = "http://127.0.0.1:5000" + add_child(ipc_client) + + # Connect signals + ipc_client.response_received.connect(_on_response_received) + simulation_manager.tick_advanced.connect(_on_tick_advanced) + + # Wait for initialization + await get_tree().process_frame + + # Connect to server + ipc_client.connect_to_server("http://127.0.0.1:5000") + +func _on_tick_advanced(tick: int): + # Gather perception data from agents + var perceptions = [] + for agent in get_tree().get_nodes_in_group("agents"): + var perception = { + "agent_id": agent.agent_id, + "tick": tick, + "position": [agent.position.x, agent.position.y, agent.position.z], + # ... other perception data + } + perceptions.append(perception) + + # Send to Python + ipc_client.send_tick_request(tick, perceptions) + +func _on_response_received(response: Dictionary): + # Execute actions from Python + var actions = response.get("actions", []) + for action in actions: + var agent = get_node("Agents/" + action["agent_id"]) + agent.execute_action(action) +``` + +### 3. Test IPC Communication + +Run the test scene: +```bash +# In Godot editor, open and run: scenes/ipc_test.gd +# Or use command line: +godot --path "c:\Projects\Agent Arena" scenes/ipc_test.gd +``` + +## Protocol Details + +### Communication Flow + +1. **Godot** advances simulation tick +2. **Godot** collects perception data from all agents +3. **Godot** sends `POST /tick` request to Python with `TickRequest` +4. **Python** receives request, distributes perceptions to agents +5. **Python** agents process observations and decide actions (LLM inference) +6. **Python** collects all actions and sends `TickResponse` +7. **Godot** receives response, executes actions in simulation +8. Repeat for next tick + +### Performance Considerations + +- **Latency**: Each tick request adds ~10-500ms depending on LLM inference time +- **Async Processing**: Python uses `asyncio` to process multiple agents concurrently +- **Batching**: All agents processed in a single HTTP request to minimize overhead +- **Timeout**: Consider implementing timeouts for slow LLM responses + +### Error Handling + +- **Connection Failures**: IPC client emits `connection_failed` signal +- **Server Errors**: Returns HTTP 500 with error details +- **Timeout**: HTTPRequest has built-in timeout (configurable) +- **Retry Logic**: Implement in Godot script as needed + +## Development Tips + +### Debugging + +1. **Enable Debug Logging**: + ```bash + python run_ipc_server.py --debug + ``` + +2. **Test Server Manually**: + ```bash + curl http://127.0.0.1:5000/health + curl -X POST http://127.0.0.1:5000/tick -H "Content-Type: application/json" -d @test_request.json + ``` + +3. **Monitor Performance**: + ```bash + curl http://127.0.0.1:5000/metrics + ``` + +### Common Issues + +1. **"Connection Failed"** + - Make sure Python IPC server is running + - Check firewall settings + - Verify port is not in use + +2. **Slow Response Times** + - LLM inference is the main bottleneck + - Use smaller/quantized models + - Increase worker pool size + - Enable response caching + +3. **JSON Parse Errors** + - Verify message format matches schemas + - Check for NaN/Inf values in float fields + - Ensure UTF-8 encoding + +## Future Enhancements + +### Planned Improvements + +1. **gRPC Protocol**: Upgrade from HTTP to gRPC for lower latency and bidirectional streaming +2. **Shared Memory**: Zero-copy IPC for maximum performance +3. **Compression**: MessagePack or Protobuf for smaller payloads +4. **Persistent Connections**: WebSocket or gRPC streaming to avoid connection overhead +5. **Load Balancing**: Distribute agents across multiple Python instances + +### Migration Path + +The current HTTP/JSON implementation is designed to be easily replaceable: +- Message schemas are decoupled from transport +- Same interfaces can be reused with different protocols +- Godot and Python can upgrade independently + +## References + +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [Godot HTTPRequest](https://docs.godotengine.org/en/stable/classes/class_httprequest.html) +- [Agent Runtime Architecture](architecture.md) diff --git a/godot/include/agent_arena.h b/godot/include/agent_arena.h index 1538d14..5b6bbf4 100644 --- a/godot/include/agent_arena.h +++ b/godot/include/agent_arena.h @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -18,6 +21,7 @@ class SimulationManager; class Agent; class EventBus; class ToolRegistry; +class IPCClient; /** * Core simulation manager that drives the deterministic tick loop @@ -153,6 +157,47 @@ class ToolRegistry : public godot::RefCounted { godot::Dictionary execute_tool(const godot::String& name, const godot::Dictionary& params); }; +/** + * IPC Client for communicating with Python agent runtime + */ +class IPCClient : public godot::Node { + GDCLASS(IPCClient, godot::Node) + +private: + godot::String server_url; + godot::HTTPRequest* http_request; + bool is_connected; + uint64_t current_tick; + godot::Dictionary pending_response; + bool response_received; + + void _on_request_completed(int result, int response_code, const godot::PackedStringArray& headers, const godot::PackedByteArray& body); + +protected: + static void _bind_methods(); + +public: + IPCClient(); + ~IPCClient(); + + void _ready() override; + void _process(double delta) override; + + // Connection management + void connect_to_server(const godot::String& url); + void disconnect_from_server(); + bool is_server_connected() const { return is_connected; } + + // Communication + void send_tick_request(uint64_t tick, const godot::Array& perceptions); + godot::Dictionary get_tick_response(); + bool has_response() const { return response_received; } + + // Getters/Setters + godot::String get_server_url() const { return server_url; } + void set_server_url(const godot::String& url); +}; + } // namespace agent_arena #endif // AGENT_ARENA_H diff --git a/godot/src/agent_arena.cpp b/godot/src/agent_arena.cpp index cab5898..8f6f761 100644 --- a/godot/src/agent_arena.cpp +++ b/godot/src/agent_arena.cpp @@ -282,3 +282,160 @@ Dictionary ToolRegistry::execute_tool(const String& name, const Dictionary& para return result; } + +// ============================================================================ +// IPCClient Implementation +// ============================================================================ + +IPCClient::IPCClient() + : server_url("http://127.0.0.1:5000"), + http_request(nullptr), + is_connected(false), + current_tick(0), + response_received(false) { +} + +IPCClient::~IPCClient() { + if (http_request != nullptr) { + http_request->queue_free(); + } +} + +void IPCClient::_bind_methods() { + ClassDB::bind_method(D_METHOD("connect_to_server", "url"), &IPCClient::connect_to_server); + ClassDB::bind_method(D_METHOD("disconnect_from_server"), &IPCClient::disconnect_from_server); + ClassDB::bind_method(D_METHOD("is_server_connected"), &IPCClient::is_server_connected); + + ClassDB::bind_method(D_METHOD("send_tick_request", "tick", "perceptions"), &IPCClient::send_tick_request); + 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("get_server_url"), &IPCClient::get_server_url); + ClassDB::bind_method(D_METHOD("set_server_url", "url"), &IPCClient::set_server_url); + + ClassDB::bind_method(D_METHOD("_on_request_completed", "result", "response_code", "headers", "body"), + &IPCClient::_on_request_completed); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "server_url"), "set_server_url", "get_server_url"); + + ADD_SIGNAL(MethodInfo("response_received", PropertyInfo(Variant::DICTIONARY, "response"))); + ADD_SIGNAL(MethodInfo("connection_failed", PropertyInfo(Variant::STRING, "error"))); +} + +void IPCClient::_ready() { + // Create HTTPRequest node + http_request = memnew(HTTPRequest); + add_child(http_request); + + // Connect signal + http_request->connect("request_completed", + Callable(this, "_on_request_completed")); + + UtilityFunctions::print("IPCClient initialized with server URL: ", server_url); +} + +void IPCClient::_process(double delta) { + // Process method for any per-frame updates +} + +void IPCClient::connect_to_server(const String& url) { + server_url = url; + + // Test connection with health check + String health_url = server_url + "/health"; + Error err = http_request->request(health_url); + + if (err != OK) { + UtilityFunctions::print("Failed to connect to server: ", server_url); + emit_signal("connection_failed", "HTTP request failed"); + is_connected = false; + } else { + UtilityFunctions::print("Connecting to IPC server: ", server_url); + } +} + +void IPCClient::disconnect_from_server() { + is_connected = false; + http_request->cancel_request(); + UtilityFunctions::print("Disconnected from IPC server"); +} + +void IPCClient::set_server_url(const String& url) { + server_url = url; +} + +void IPCClient::send_tick_request(uint64_t tick, const Array& perceptions) { + if (!is_connected) { + UtilityFunctions::print("Warning: Sending request while not connected"); + } + + current_tick = tick; + response_received = false; + + // Build request JSON + Dictionary request_dict; + request_dict["tick"] = tick; + request_dict["perceptions"] = perceptions; + request_dict["simulation_state"] = Dictionary(); + + String json = JSON::stringify(request_dict); + + // Send POST request + String url = server_url + "/tick"; + PackedStringArray headers; + headers.append("Content-Type: application/json"); + + Error err = http_request->request(url, headers, HTTPClient::METHOD_POST, json); + + if (err != OK) { + UtilityFunctions::print("Error sending tick request: ", err); + } +} + +Dictionary IPCClient::get_tick_response() { + if (response_received) { + response_received = false; + return pending_response; + } + return Dictionary(); +} + +void IPCClient::_on_request_completed(int result, int response_code, + const PackedStringArray& headers, + const PackedByteArray& body) { + if (result != HTTPRequest::RESULT_SUCCESS) { + UtilityFunctions::print("HTTP Request failed with result: ", result); + emit_signal("connection_failed", "Request failed"); + is_connected = false; + return; + } + + if (response_code == 200) { + // Parse JSON response + String body_string = body.get_string_from_utf8(); + + // Parse JSON + JSON json; + Error err = json.parse(body_string); + + if (err == OK) { + Variant data = json.get_data(); + if (data.get_type() == Variant::DICTIONARY) { + pending_response = data; + response_received = true; + is_connected = true; + + emit_signal("response_received", pending_response); + + UtilityFunctions::print("Received tick response for tick ", current_tick); + } else { + UtilityFunctions::print("Invalid JSON response format"); + } + } else { + UtilityFunctions::print("Failed to parse JSON response"); + } + } else { + UtilityFunctions::print("HTTP request returned error code: ", response_code); + is_connected = false; + } +} diff --git a/godot/src/register_types.cpp b/godot/src/register_types.cpp index fd83793..e3ae5f2 100644 --- a/godot/src/register_types.cpp +++ b/godot/src/register_types.cpp @@ -18,6 +18,7 @@ void initialize_agent_arena_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); } void uninitialize_agent_arena_module(ModuleInitializationLevel p_level) { diff --git a/python/.gdignore b/python/.gdignore new file mode 100644 index 0000000..1a58176 --- /dev/null +++ b/python/.gdignore @@ -0,0 +1,2 @@ +// This file tells Godot to ignore the entire python/ directory +// The Python code is run separately via IPC, not imported by Godot diff --git a/python/ipc/__init__.py b/python/ipc/__init__.py new file mode 100644 index 0000000..8f37582 --- /dev/null +++ b/python/ipc/__init__.py @@ -0,0 +1,8 @@ +""" +IPC module for communication between Godot and Python. +""" + +from .server import IPCServer +from .messages import PerceptionMessage, ActionMessage, TickRequest, TickResponse + +__all__ = ["IPCServer", "PerceptionMessage", "ActionMessage", "TickRequest", "TickResponse"] diff --git a/python/ipc/messages.py b/python/ipc/messages.py new file mode 100644 index 0000000..43d30e9 --- /dev/null +++ b/python/ipc/messages.py @@ -0,0 +1,151 @@ +""" +Message schemas for IPC communication between Godot and Python. + +These define the structure of data exchanged during simulation ticks. +""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class PerceptionMessage: + """ + Perception data sent from Godot to Python for a single agent. + + Contains all observations the agent receives from the simulation. + """ + agent_id: str + tick: int + position: list[float] # [x, y, z] + rotation: list[float] # [x, y, z] euler angles + velocity: list[float] = field(default_factory=lambda: [0.0, 0.0, 0.0]) + visible_entities: list[dict[str, Any]] = field(default_factory=list) + inventory: list[dict[str, Any]] = field(default_factory=list) + health: float = 100.0 + energy: float = 100.0 + custom_data: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "PerceptionMessage": + """Create PerceptionMessage from dictionary.""" + return cls( + agent_id=data["agent_id"], + tick=data["tick"], + position=data.get("position", [0.0, 0.0, 0.0]), + rotation=data.get("rotation", [0.0, 0.0, 0.0]), + velocity=data.get("velocity", [0.0, 0.0, 0.0]), + visible_entities=data.get("visible_entities", []), + inventory=data.get("inventory", []), + health=data.get("health", 100.0), + energy=data.get("energy", 100.0), + custom_data=data.get("custom_data", {}), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "agent_id": self.agent_id, + "tick": self.tick, + "position": self.position, + "rotation": self.rotation, + "velocity": self.velocity, + "visible_entities": self.visible_entities, + "inventory": self.inventory, + "health": self.health, + "energy": self.energy, + "custom_data": self.custom_data, + } + + +@dataclass +class ActionMessage: + """ + Action decision sent from Python to Godot for a single agent. + + Contains the tool call and parameters for the agent to execute. + """ + agent_id: str + tick: int + tool: str + params: dict[str, Any] = field(default_factory=dict) + reasoning: str = "" # Optional explanation of decision + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ActionMessage": + """Create ActionMessage from dictionary.""" + return cls( + agent_id=data["agent_id"], + tick=data["tick"], + tool=data["tool"], + params=data.get("params", {}), + reasoning=data.get("reasoning", ""), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "agent_id": self.agent_id, + "tick": self.tick, + "tool": self.tool, + "params": self.params, + "reasoning": self.reasoning, + } + + +@dataclass +class TickRequest: + """ + Request sent from Godot to Python containing all agent perceptions for a tick. + """ + tick: int + perceptions: list[PerceptionMessage] + simulation_state: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "TickRequest": + """Create TickRequest from dictionary.""" + perceptions = [ + PerceptionMessage.from_dict(p) for p in data.get("perceptions", []) + ] + return cls( + tick=data["tick"], + perceptions=perceptions, + simulation_state=data.get("simulation_state", {}), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "tick": self.tick, + "perceptions": [p.to_dict() for p in self.perceptions], + "simulation_state": self.simulation_state, + } + + +@dataclass +class TickResponse: + """ + Response sent from Python to Godot containing all agent actions for a tick. + """ + tick: int + actions: list[ActionMessage] + metrics: dict[str, Any] = field(default_factory=dict) # Performance metrics + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "TickResponse": + """Create TickResponse from dictionary.""" + actions = [ActionMessage.from_dict(a) for a in data.get("actions", [])] + return cls( + tick=data["tick"], + actions=actions, + metrics=data.get("metrics", {}), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "tick": self.tick, + "actions": [a.to_dict() for a in self.actions], + "metrics": self.metrics, + } diff --git a/python/ipc/server.py b/python/ipc/server.py new file mode 100644 index 0000000..9129b74 --- /dev/null +++ b/python/ipc/server.py @@ -0,0 +1,249 @@ +""" +IPC Server - FastAPI server for handling Godot <-> Python communication. + +This server receives perception data from Godot, processes agent decisions, +and returns actions to execute in the simulation. +""" + +import asyncio +import logging +import time +from contextlib import asynccontextmanager +from typing import Any + +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse + +from agent_runtime.runtime import AgentRuntime +from .messages import ActionMessage, TickRequest, TickResponse + +logger = logging.getLogger(__name__) + + +class IPCServer: + """ + IPC Server for handling communication between Godot and Python. + + Runs a FastAPI server that receives tick requests and returns agent actions. + """ + + def __init__( + self, + runtime: AgentRuntime, + host: str = "127.0.0.1", + port: int = 5000, + ): + """ + Initialize the IPC server. + + Args: + runtime: AgentRuntime instance to process agent decisions + host: Host address to bind to + port: Port to listen on + """ + self.runtime = runtime + self.host = host + self.port = port + self.app: FastAPI | None = None + self.metrics = { + "total_ticks": 0, + "total_agents_processed": 0, + "avg_tick_time_ms": 0.0, + } + + def create_app(self) -> FastAPI: + """Create and configure the FastAPI application.""" + + @asynccontextmanager + async def lifespan(app: FastAPI): + """Lifespan context manager for startup/shutdown.""" + logger.info("Starting IPC server...") + self.runtime.start() + yield + logger.info("Shutting down IPC server...") + self.runtime.stop() + + app = FastAPI( + title="Agent Arena IPC Server", + description="Communication bridge between Godot simulation and Python agents", + version="0.1.0", + lifespan=lifespan, + ) + + @app.get("/") + async def root(): + """Health check endpoint.""" + return { + "status": "running", + "agents": len(self.runtime.agents), + "metrics": self.metrics, + } + + @app.get("/health") + async def health(): + """Health check endpoint.""" + return {"status": "ok", "agents": len(self.runtime.agents)} + + @app.post("/tick") + async def process_tick(request_data: dict[str, Any]) -> dict[str, Any]: + """ + Process a simulation tick. + + Receives perception data for all agents, processes decisions, + and returns actions to execute. + + Args: + request_data: Tick request containing agent perceptions + + Returns: + Tick response containing agent actions + """ + start_time = time.time() + + try: + # Parse request + tick_request = TickRequest.from_dict(request_data) + tick = tick_request.tick + + logger.debug( + f"Processing tick {tick} with {len(tick_request.perceptions)} agents" + ) + + # Build observations dict for runtime + observations = {} + for perception in tick_request.perceptions: + observations[perception.agent_id] = { + "tick": perception.tick, + "position": perception.position, + "rotation": perception.rotation, + "velocity": perception.velocity, + "visible_entities": perception.visible_entities, + "inventory": perception.inventory, + "health": perception.health, + "energy": perception.energy, + "custom_data": perception.custom_data, + } + + # Process agents and get actions + actions_dict = await self.runtime.process_tick(tick, observations) + + # Convert actions to response format + action_messages = [] + for agent_id, action in actions_dict.items(): + action_msg = ActionMessage( + agent_id=agent_id, + tick=tick, + tool=action.tool, + params=action.params, + reasoning=action.reasoning, + ) + action_messages.append(action_msg) + + # Calculate metrics + elapsed_ms = (time.time() - start_time) * 1000 + self.metrics["total_ticks"] += 1 + self.metrics["total_agents_processed"] += len(observations) + self.metrics["avg_tick_time_ms"] = ( + self.metrics["avg_tick_time_ms"] * 0.9 + elapsed_ms * 0.1 + ) + + # Build response + response = TickResponse( + tick=tick, + actions=action_messages, + metrics={ + "tick_time_ms": elapsed_ms, + "agents_processed": len(observations), + "actions_generated": len(action_messages), + }, + ) + + logger.debug( + f"Tick {tick} processed in {elapsed_ms:.2f}ms, " + f"{len(action_messages)} actions generated" + ) + + return response.to_dict() + + except Exception as e: + logger.error(f"Error processing tick: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + @app.post("/agents/register") + async def register_agent(agent_data: dict[str, Any]) -> dict[str, str]: + """ + Register a new agent with the runtime. + + Args: + agent_data: Agent configuration data + + Returns: + Success message with agent ID + """ + try: + agent_id = agent_data.get("agent_id") + if not agent_id: + raise HTTPException(status_code=400, detail="agent_id is required") + + # Note: Agent instantiation would happen here + # For now, we'll just acknowledge the registration + logger.info(f"Agent registration request received for {agent_id}") + + return {"status": "success", "agent_id": agent_id} + + except Exception as e: + logger.error(f"Error registering agent: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @app.get("/metrics") + async def get_metrics(): + """Get server performance metrics.""" + return self.metrics + + self.app = app + return app + + def run(self): + """Run the IPC server (blocking).""" + import uvicorn + + if not self.app: + self.create_app() + + logger.info(f"Starting IPC server on {self.host}:{self.port}") + uvicorn.run(self.app, host=self.host, port=self.port, log_level="info") + + async def run_async(self): + """Run the IPC server asynchronously.""" + import uvicorn + + if not self.app: + self.create_app() + + config = uvicorn.Config( + self.app, host=self.host, port=self.port, log_level="info" + ) + server = uvicorn.Server(config) + await server.serve() + + +def create_server( + runtime: AgentRuntime | None = None, + host: str = "127.0.0.1", + port: int = 5000, +) -> IPCServer: + """ + Factory function to create an IPC server. + + Args: + runtime: AgentRuntime instance, or None to create a default one + host: Host address to bind to + port: Port to listen on + + Returns: + Configured IPCServer instance + """ + if runtime is None: + runtime = AgentRuntime(max_workers=4) + + return IPCServer(runtime=runtime, host=host, port=port) diff --git a/python/requirements-minimal.txt b/python/requirements-minimal.txt new file mode 100644 index 0000000..f4a52a9 --- /dev/null +++ b/python/requirements-minimal.txt @@ -0,0 +1,14 @@ +# Minimal requirements for IPC testing with Python 3.14 +# This is a temporary file - use requirements.txt with Python 3.11 for full functionality + +# IPC Server (essential for testing) +fastapi>=0.104.0 +uvicorn>=0.24.0 + +# Core dependencies +numpy>=1.24.0 +pydantic>=2.0.0 + +# Testing +pytest>=7.4.0 +pytest-asyncio>=0.21.0 diff --git a/python/requirements.txt b/python/requirements.txt index 3248992..2f9a6df 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -5,6 +5,10 @@ msgpack>=1.0.5 hydra-core>=1.3.0 omegaconf>=2.3.0 +# IPC Server +fastapi>=0.104.0 +uvicorn>=0.24.0 + # LLM backends llama-cpp-python>=0.2.0 # llama.cpp Python bindings openai>=1.0.0 # For OpenAI-compatible APIs diff --git a/python/run_ipc_server.py b/python/run_ipc_server.py new file mode 100644 index 0000000..820119d --- /dev/null +++ b/python/run_ipc_server.py @@ -0,0 +1,83 @@ +""" +Startup script for the Agent Arena IPC server. + +This script starts the FastAPI server that handles communication +between Godot and Python agents. +""" + +import argparse +import logging +import sys + +from agent_runtime.runtime import AgentRuntime +from ipc.server import create_server + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) + +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser( + description="Agent Arena IPC Server - Communication bridge between Godot and Python" + ) + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + help="Host address to bind to (default: 127.0.0.1)", + ) + parser.add_argument( + "--port", + type=int, + default=5000, + help="Port to listen on (default: 5000)", + ) + parser.add_argument( + "--workers", + type=int, + default=4, + help="Maximum number of concurrent agent workers (default: 4)", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug logging", + ) + + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + logger.info("=" * 60) + logger.info("Agent Arena IPC Server") + logger.info("=" * 60) + logger.info(f"Host: {args.host}") + logger.info(f"Port: {args.port}") + logger.info(f"Max Workers: {args.workers}") + logger.info("=" * 60) + + try: + # Create runtime + runtime = AgentRuntime(max_workers=args.workers) + + # Create and start server + server = create_server(runtime=runtime, host=args.host, port=args.port) + logger.info("Starting IPC server...") + server.run() + + except KeyboardInterrupt: + logger.info("\nShutting down gracefully...") + sys.exit(0) + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scenes/test_IPC.tscn b/scenes/test_IPC.tscn new file mode 100644 index 0000000..dd1f827 --- /dev/null +++ b/scenes/test_IPC.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://b2p3ubgvl4wrw"] + +[node name="Node2D" type="Node2D"] diff --git a/scenes/test_arena.gd b/scenes/test_arena.gd deleted file mode 100644 index e5dc5c3..0000000 --- a/scenes/test_arena.gd +++ /dev/null @@ -1,99 +0,0 @@ -extends Node - -@onready var simulation_manager = $SimulationManager -@onready var agent = $Agent -@onready var label = $UI/Label - -var tick_count = 0 - -func _ready(): - print("Test Arena Ready!") - - # Connect simulation signals - simulation_manager.simulation_started.connect(_on_simulation_started) - simulation_manager.simulation_stopped.connect(_on_simulation_stopped) - simulation_manager.tick_advanced.connect(_on_tick_advanced) - - # Connect agent signals - agent.action_decided.connect(_on_agent_action_decided) - agent.perception_received.connect(_on_agent_perception_received) - - # Set up agent - agent.agent_id = "test_agent_001" - - print("SimulationManager: ", simulation_manager) - print("Agent: ", agent) - print("Agent ID: ", agent.agent_id) - -func _input(event): - if event is InputEventKey and event.pressed: - if event.keycode == KEY_SPACE: - if simulation_manager.is_running: - simulation_manager.stop_simulation() - else: - simulation_manager.start_simulation() - elif event.keycode == KEY_R: - simulation_manager.reset_simulation() - elif event.keycode == KEY_S: - simulation_manager.step_simulation() - elif event.keycode == KEY_T: - test_agent() - -func _process(_delta): - if simulation_manager.is_running: - label.text = "Agent Arena Test Scene (RUNNING) -Tick: %d -Agent ID: %s -Press SPACE to stop -Press S to step -Press R to reset -Press T to test agent" % [simulation_manager.current_tick, agent.agent_id] - else: - label.text = "Agent Arena Test Scene (STOPPED) -Tick: %d -Agent ID: %s -Press SPACE to start -Press S to step -Press R to reset -Press T to test agent" % [simulation_manager.current_tick, agent.agent_id] - -func test_agent(): - print("Testing agent functions...") - - # Test perception - var obs = { - "position": Vector3(10, 0, 5), - "health": 100, - "nearby_objects": ["tree", "rock", "water"] - } - agent.perceive(obs) - - # Test decision - var action = agent.decide_action() - print("Agent decided action: ", action) - - # Test memory - agent.store_memory("test_key", "test_value") - var retrieved = agent.retrieve_memory("test_key") - print("Retrieved memory: ", retrieved) - - # Test tool call - var tool_result = agent.call_tool("move", {"direction": "north", "distance": 5}) - print("Tool result: ", tool_result) - -func _on_simulation_started(): - print("✓ Simulation started!") - -func _on_simulation_stopped(): - print("✓ Simulation stopped!") - -func _on_tick_advanced(tick): - tick_count += 1 - if tick_count % 60 == 0: # Print every 60 ticks (1 second) - print("Tick: ", tick) - -func _on_agent_action_decided(action): - print("Agent action: ", action) - -func _on_agent_perception_received(observations): - print("Agent perceived: ", observations) diff --git a/scenes/test_arena.gd.uid b/scenes/test_arena.gd.uid deleted file mode 100644 index 67fff9c..0000000 --- a/scenes/test_arena.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cdr05mqnvdoyp diff --git a/scenes/test_arena.tscn b/scenes/test_arena.tscn index 7f3ee6d..d346ec8 100644 --- a/scenes/test_arena.tscn +++ b/scenes/test_arena.tscn @@ -1,6 +1,6 @@ -[gd_scene load_steps=2 format=3 uid="uid://test_arena_001"] +[gd_scene load_steps=2 format=3] -[ext_resource type="Script" path="res://scenes/test_arena.gd" id="1_test"] +[ext_resource type="Script" uid="uid://dosf7di72b72k" path="res://scripts/test_arena.gd" id="1_test"] [node name="TestArena" type="Node"] script = ExtResource("1_test")