Skip to content

Commit 8ef66f1

Browse files
committed
Merge branch 'JustinDev'
2 parents fc4361a + 3e6967b commit 8ef66f1

1 file changed

Lines changed: 304 additions & 2 deletions

File tree

docs/backlog_items.md

Lines changed: 304 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,11 +614,313 @@ Run benchmarks on mobile devices.
614614

615615
---
616616

617+
---
618+
619+
#### B-31: LocalLLMBehavior - Bridge Local Backends to AgentBehavior API
620+
**Priority**: High
621+
**Component**: Agent Runtime / Backends
622+
**Size**: M
623+
**Blocking**: LLM agent integration with foraging scene
624+
625+
**Problem Statement**:
626+
The codebase has two separate agent systems that aren't connected:
627+
628+
1. **AgentBehavior system** (`python/agent_runtime/behavior.py`)
629+
- Classes: `AgentBehavior`, `SimpleAgentBehavior`, `LLMAgentBehavior`
630+
- Used by: IPC server's `/observe` endpoint via `server.behaviors` dict
631+
- Learner-facing API with three tiers (beginner/intermediate/advanced)
632+
633+
2. **Local LLM Backend system** (`python/backends/`)
634+
- Classes: `LlamaCppBackend`, `VLLMBackend`
635+
- Used by: `run_ipc_server_with_gpu.py` via `Agent` class
636+
- GPU-accelerated local inference
637+
638+
**Current Flow (Broken)**:
639+
```
640+
Godot foraging scene
641+
↓ POST /observe (sends observation)
642+
python/ipc/server.py line 424-475
643+
↓ checks server.behaviors dict → EMPTY
644+
↓ falls back to mock rule-based logic
645+
(LlamaCppBackend never gets called)
646+
```
647+
648+
**Goal**: Create `LocalLLMBehavior` class that wraps local backends (LlamaCppBackend, VLLMBackend) and implements the `AgentBehavior` interface, so local LLMs can power agents via the `/observe` endpoint.
649+
650+
---
651+
652+
**Architecture Context**:
653+
654+
The IPC server (`python/ipc/server.py`) has a `behaviors` dict that maps `agent_id → AgentBehavior`. When `/observe` receives an observation:
655+
- Line 424-426: Gets agent_id from observation
656+
- Line 427: Checks `behavior = self.behaviors.get(agent_id)`
657+
- Line 428-471: If behavior exists, calls `behavior.decide(observation, tools)`
658+
- Line 472-475: If no behavior, falls back to `_make_mock_decision()`
659+
660+
The `LLMAgentBehavior` class (line 281-422 in behavior.py) already supports cloud LLMs (Anthropic, OpenAI, Ollama) but NOT the local backends (LlamaCppBackend, VLLMBackend).
661+
662+
---
663+
664+
**Files to Understand First**:
665+
666+
1. `python/agent_runtime/behavior.py` - Base classes, especially `LLMAgentBehavior`
667+
2. `python/backends/base.py` - `BaseBackend` interface with `generate()` and `generate_with_tools()`
668+
3. `python/backends/llama_cpp_backend.py` - Local GPU inference implementation
669+
4. `python/backends/vllm_backend.py` - vLLM server client
670+
5. `python/ipc/server.py` - See `/observe` endpoint (line 397-493) and `create_server()` function
671+
6. `python/user_agents/examples/llm_forager.py` - Example of LLMAgentBehavior subclass
672+
7. `python/scenarios/foraging.py` - Scenario definition with `to_system_prompt()` method
673+
674+
---
675+
676+
**Implementation Tasks**:
677+
678+
- [ ] **Create `python/agent_runtime/local_llm_behavior.py`**
679+
680+
```python
681+
"""
682+
Local LLM behavior adapter.
683+
684+
Bridges the AgentBehavior interface to local LLM backends
685+
(LlamaCppBackend, VLLMBackend) for GPU-accelerated inference.
686+
"""
687+
688+
from agent_runtime.behavior import AgentBehavior
689+
from agent_runtime.schemas import AgentDecision, Observation, ToolSchema
690+
from backends.base import BaseBackend
691+
692+
class LocalLLMBehavior(AgentBehavior):
693+
"""
694+
AgentBehavior implementation using local LLM backends.
695+
696+
This bridges the learner-facing AgentBehavior API to the
697+
high-performance local backends (llama.cpp, vLLM).
698+
699+
Example:
700+
from backends import LlamaCppBackend, BackendConfig
701+
from agent_runtime.local_llm_behavior import LocalLLMBehavior
702+
703+
config = BackendConfig(model_path="models/llama.gguf", n_gpu_layers=-1)
704+
backend = LlamaCppBackend(config)
705+
706+
behavior = LocalLLMBehavior(
707+
backend=backend,
708+
system_prompt="You are a foraging agent..."
709+
)
710+
711+
# Register with IPC server
712+
server = create_server(behaviors={"agent_001": behavior})
713+
"""
714+
715+
def __init__(
716+
self,
717+
backend: BaseBackend,
718+
system_prompt: str = "",
719+
temperature: float = 0.7,
720+
max_tokens: int = 256,
721+
):
722+
self.backend = backend
723+
self.system_prompt = system_prompt
724+
self.temperature = temperature
725+
self.max_tokens = max_tokens
726+
self._memory: list[Observation] = []
727+
self._memory_capacity = 10
728+
729+
def decide(self, observation: Observation, tools: list[ToolSchema]) -> AgentDecision:
730+
"""Use local LLM to decide next action."""
731+
# 1. Store observation in memory
732+
# 2. Build prompt from system_prompt + observation + tools
733+
# 3. Call backend.generate_with_tools() or backend.generate()
734+
# 4. Parse response into AgentDecision
735+
# 5. Handle errors gracefully (return idle on failure)
736+
pass
737+
738+
def _build_prompt(self, observation: Observation, tools: list[ToolSchema]) -> str:
739+
"""Build the prompt for the LLM."""
740+
# Include: system prompt, current observation, available tools, memory
741+
pass
742+
743+
def _parse_response(self, response: str, tools: list[ToolSchema]) -> AgentDecision:
744+
"""Parse LLM response into AgentDecision."""
745+
# Handle JSON parsing, validate tool names, extract params
746+
pass
747+
748+
def on_episode_start(self) -> None:
749+
"""Clear memory at episode start."""
750+
self._memory.clear()
751+
```
752+
753+
- [ ] **Add factory function for easy creation**
754+
755+
```python
756+
def create_local_llm_behavior(
757+
model_path: str,
758+
backend_type: str = "llama_cpp", # or "vllm"
759+
n_gpu_layers: int = -1,
760+
system_prompt: str = "",
761+
**kwargs
762+
) -> LocalLLMBehavior:
763+
"""Factory to create LocalLLMBehavior with backend."""
764+
pass
765+
```
766+
767+
- [ ] **Update `python/ipc/server.py` `create_server()` function** (line 520-541)
768+
769+
Add optional parameter to auto-create local LLM behavior:
770+
```python
771+
def create_server(
772+
runtime: AgentRuntime | None = None,
773+
behaviors: dict | None = None,
774+
host: str = "127.0.0.1",
775+
port: int = 5000,
776+
default_behavior: AgentBehavior | None = None, # NEW: fallback for unknown agents
777+
) -> IPCServer:
778+
```
779+
780+
- [ ] **Create `python/run_local_llm_forager.py`** - Startup script
781+
782+
```python
783+
"""
784+
Run foraging scene with local LLM agent.
785+
786+
Usage:
787+
python run_local_llm_forager.py --model models/llama.gguf --gpu-layers -1
788+
"""
789+
790+
# 1. Load local backend (LlamaCppBackend or VLLMBackend)
791+
# 2. Create LocalLLMBehavior with foraging system prompt
792+
# 3. Register behavior for agent_id matching Godot scene
793+
# 4. Start IPC server
794+
```
795+
796+
- [ ] **Integrate scenario system prompts**
797+
798+
Use `python/scenarios/foraging.py` to generate system prompts:
799+
```python
800+
from scenarios import get_scenario
801+
802+
scenario = get_scenario("foraging")
803+
system_prompt = scenario.to_system_prompt(include_hints=True)
804+
behavior = LocalLLMBehavior(backend=backend, system_prompt=system_prompt)
805+
```
806+
807+
- [ ] **Add to `__init__.py` exports**
808+
809+
Update `python/agent_runtime/__init__.py` to export `LocalLLMBehavior`
810+
811+
- [ ] **Write tests**
812+
813+
Create `tests/test_local_llm_behavior.py`:
814+
- Test prompt building
815+
- Test response parsing (valid JSON, invalid JSON, missing fields)
816+
- Test tool validation
817+
- Test memory management
818+
- Mock backend for unit tests
819+
820+
---
821+
822+
**Reference: How LLMAgentBehavior works** (for comparison):
823+
824+
```python
825+
# From behavior.py lines 321-338
826+
def complete(self, prompt: str, system: str | None = None, temperature: float = 0.7) -> str:
827+
if self._client is None:
828+
self._client = self._create_client()
829+
sys_prompt = system if system is not None else self.system_prompt
830+
return self._call_llm(prompt, sys_prompt, temperature)
831+
832+
# _call_llm handles Anthropic/OpenAI/Ollama APIs
833+
```
834+
835+
LocalLLMBehavior should follow similar pattern but call:
836+
```python
837+
result = self.backend.generate_with_tools(prompt, tools_as_dicts, temperature)
838+
# or
839+
result = self.backend.generate(prompt, temperature, max_tokens)
840+
```
841+
842+
---
843+
844+
**Reference: Backend API** (from `backends/base.py`):
845+
846+
```python
847+
class BaseBackend(ABC):
848+
@abstractmethod
849+
def generate(self, prompt: str, temperature: float | None, max_tokens: int | None) -> GenerationResult:
850+
pass
851+
852+
@abstractmethod
853+
def generate_with_tools(self, prompt: str, tools: list[dict], temperature: float | None) -> GenerationResult:
854+
pass
855+
856+
@dataclass
857+
class GenerationResult:
858+
text: str
859+
tokens_used: int
860+
finish_reason: str
861+
metadata: dict[str, Any]
862+
```
863+
864+
---
865+
866+
**Agent ID Matching**:
867+
868+
The Godot foraging scene uses `SimpleAgent` which has an `agent_id` property:
869+
- If set in scene: uses that value
870+
- If empty: auto-generates `"agent_" + timestamp`
871+
872+
For testing, either:
873+
1. Set `agent_id = "forager_001"` in the Godot scene's SimpleAgent node
874+
2. OR use a wildcard/default behavior in the server
875+
876+
Recommend option 1 for explicit control.
877+
878+
---
879+
880+
**Acceptance Criteria**:
881+
882+
- [ ] `LocalLLMBehavior` implements full `AgentBehavior` interface
883+
- [ ] Works with both `LlamaCppBackend` and `VLLMBackend`
884+
- [ ] Integrates with scenario system prompts (`to_system_prompt()`)
885+
- [ ] `run_local_llm_forager.py` successfully runs foraging scene with local LLM
886+
- [ ] Agent makes reasonable decisions (moves to resources, avoids hazards)
887+
- [ ] Graceful error handling (returns idle on LLM failures)
888+
- [ ] All tests pass
889+
- [ ] Pre-commit hooks pass (black, ruff, mypy)
890+
891+
---
892+
893+
**Testing Instructions**:
894+
895+
1. Download a GGUF model (e.g., `llama-2-7b-chat.Q4_K_M.gguf`)
896+
2. Run: `python run_local_llm_forager.py --model path/to/model.gguf`
897+
3. Open Godot and run the foraging scene
898+
4. Observe agent behavior in console logs
899+
5. Verify decisions are LLM-generated (not mock logic)
900+
901+
---
902+
903+
**Related Files Summary**:
904+
905+
| File | Purpose |
906+
|------|---------|
907+
| `python/agent_runtime/behavior.py` | Base classes to extend |
908+
| `python/backends/llama_cpp_backend.py` | Local GPU backend |
909+
| `python/backends/vllm_backend.py` | vLLM server backend |
910+
| `python/backends/base.py` | Backend interface |
911+
| `python/ipc/server.py` | IPC server with /observe endpoint |
912+
| `python/scenarios/foraging.py` | System prompt source |
913+
| `python/user_agents/examples/llm_forager.py` | Reference implementation |
914+
| `scripts/simple_agent.gd` | Godot agent (sends agent_id) |
915+
| `scripts/base_scene_controller.gd` | Sends observations to /observe |
916+
917+
---
918+
617919
## Total Backlog Summary
618920

619-
- **High Priority**: 7 items
921+
- **High Priority**: 8 items
620922
- **Medium Priority**: 15 items
621923
- **Low Priority**: 8 items
622-
- **Total**: 30 items
924+
- **Total**: 31 items
623925

624926
**Estimated Timeline**: 6-12 months for all items with 2 developers

0 commit comments

Comments
 (0)