Skip to content

Commit 9b16637

Browse files
authored
Merge pull request #37 from renbytes/dev-canonicalStudies
seed schelling sim
2 parents 8195bc8 + b7a4b0c commit 9b16637

207 files changed

Lines changed: 3695 additions & 11489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ DATABASE_URL=postgresql+asyncpg://admin:password@host.docker.internal:5432/agent
1313

1414
MLFLOW_TRACKING_USERNAME=admin
1515
MLFLOW_TRACKING_PASSWORD=password
16-
MLFLOW_TRACKING_URI=http://mlflow:5000
16+
MLFLOW_TRACKING_URI=http://localhost:5001
1717
MLFLOW_HTTP_REQUEST_TIMEOUT=10
1818

1919
# --- Optional Services ---
2020
# NOTE: Redis can still use its service name as it's typically more stable
21-
REDIS_URL=redis://redis:6379/0
21+
REDIS_URL=redis://localhost:6379/0
2222

2323
# --- OpenAI API Key (Developer must provide their own) ---
2424
OPENAI_API_KEY=sk-{your_openai_api_key_here}

‎Dockerfile‎

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
1-
# Pin the Python version to match your poetry.lock file
1+
# Use an official Python base image
22
FROM python:3.11.9-slim
33

4-
# Set the working directory inside the container
5-
WORKDIR /app
6-
7-
# Use modern ENV syntax
4+
# Configure environment variables for Python and Poetry
85
ENV PYTHONDONTWRITEBYTECODE=1
96
ENV PYTHONUNBUFFERED=1
10-
ENV POETRY_NO_INTERACTION=1 \
11-
POETRY_VIRTUALENVS_IN_PROJECT=1 \
12-
POETRY_VIRTUALENVS_CREATE=1
7+
ENV POETRY_HOME="/opt/poetry"
8+
ENV POETRY_NO_INTERACTION=1
9+
ENV POETRY_VIRTUALENVS_CREATE=false
1310

14-
# Install build tools, git, AND the graphviz development library
15-
RUN apt-get update && apt-get install -y build-essential git graphviz-dev
11+
# Add Poetry's bin directory to the system PATH.
12+
ENV PATH="$POETRY_HOME/bin:$PATH"
1613

17-
# Install Poetry
18-
RUN pip install --no-cache-dir poetry
14+
# Set the working directory
15+
WORKDIR /app
1916

20-
# Copy dependency files first
21-
COPY pyproject.toml poetry.lock ./
22-
COPY agent-sim/pyproject.toml ./agent-sim/
17+
# Install system dependencies and Poetry itself
18+
RUN apt-get update \
19+
&& apt-get install -y --no-install-recommends curl \
20+
&& curl -sSL https://install.python-poetry.org | python - \
21+
&& apt-get remove -y curl \
22+
&& apt-get autoremove -y \
23+
&& rm -rf /var/lib/apt/lists/*
2324

24-
# Copy the entire project source code BEFORE running install
25+
# Copy ALL project files before installing dependencies.
26+
# This ensures Poetry can find the local path dependencies (agent-core, etc.).
2527
COPY . .
2628

27-
# Install project dependencies using the lock file
28-
RUN poetry install --no-root
29+
# Install build dependencies, install Python packages, then remove build dependencies.
30+
# This ensures packages that need compilation (like ecos, osqp) can be built,
31+
# while keeping the final image size smaller.
32+
RUN apt-get update \
33+
&& apt-get install -y --no-install-recommends build-essential cmake git graphviz libgraphviz-dev \
34+
&& poetry install --without dev \
35+
&& apt-get purge -y --auto-remove build-essential cmake git graphviz libgraphviz-dev \
36+
&& rm -rf /var/lib/apt/lists/*
2937

30-
# The command to run when the container starts
38+
# The default command to run when the container starts
3139
CMD ["tail", "-f", "/dev/null"]

‎Makefile‎

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,114 @@
1-
.PHONY: setup up down run-example logs
1+
# ==============================================================================
2+
# ARLA Project Makefile
3+
#
4+
# Provides a simplified interface for managing the Docker-based development
5+
# environment and running simulations.
6+
# ==============================================================================
27

3-
# Default target
4-
all: up
8+
.PHONY: help setup up down logs init-db run run-local cli test lint make-gif
9+
.DEFAULT_GOAL := help
510

6-
# Set up the environment by copying the example .env file
7-
setup:
8-
@if [ ! -f .env ]; then \
9-
echo "--- Setup: Creating .env file from .env.example..."; \
10-
cp .env.example .env; \
11-
else \
12-
echo "--- Setup: .env file already exists."; \
13-
fi
14-
15-
# Build and start all services in the background
16-
up: setup
17-
@echo "--- Starting all services with Docker Compose..."
18-
docker compose up --build -d
19-
20-
# Stop and remove all services
21-
down:
22-
@echo "--- Stopping all services..."
23-
docker compose down
11+
# --- Variable Definitions ---
12+
# These variables can be overridden from the command line.
13+
# Example: make run-local PACKAGE=simulations.new_sim ...
14+
15+
# The Python package of the simulation to run.
16+
PACKAGE ?= simulations.schelling_sim
17+
# Path to the base YAML config file for the simulation.
18+
CONFIG ?= simulations/schelling_sim/config/config.yml
19+
# Path to the scenario JSON file or experiment YAML file.
20+
FILE ?= simulations/schelling_sim/scenarios/default.json
21+
# Number of simulation steps for local runs.
22+
STEPS ?= 200
23+
# Generic arguments for the CLI.
24+
ARGS ?=
25+
# Default directory for rendering output.
26+
RENDER_DIR ?= data/gif_renders
27+
# Default frames per second for rendering GIFs.
28+
FPS ?= 15
29+
30+
31+
# --- Core Docker Commands ---
2432

25-
# Use the direct path to the venv's python executable
26-
run-example:
27-
@echo "--- Running: Executing example scenario inside Docker..."
28-
docker compose exec app /app/.venv/bin/python -m agent_sim.main --scenario simulations/soul_sim/scenarios/default.json --steps 50
33+
## up: Build images and start all services in the background.
34+
up:
35+
@echo "🚀 Building images and starting all services..."
36+
@docker compose up -d --build
2937

30-
# View the logs from all running services
38+
## down: Stop and remove all containers, networks, and volumes.
39+
down:
40+
@echo "🛑 Stopping and removing all services..."
41+
@docker compose down -v
42+
43+
## logs: View and follow live logs from all running services.
3144
logs:
32-
@echo "--- Tailing logs from all services..."
33-
docker compose logs -f
45+
@echo "📜 Tailing logs..."
46+
@docker compose logs -f
47+
48+
## init-db: Connects to the DB and creates all necessary tables.
49+
init-db:
50+
@echo "Initializing database and creating tables..."
51+
@docker compose exec app poetry run python -m agent_sim.infrastructure.database.init_db
52+
53+
## setup: Create the .env file from the example template.
54+
setup:
55+
@echo "📋 Copying .env.example to .env..."
56+
@cp .env.example .env
57+
@echo "✅ Done. Please add your OPENAI_API_KEY to the .env file."
58+
59+
60+
# --- Simulation & Development Commands ---
61+
62+
## run: Run a full experiment, submitting jobs to the Celery queue.
63+
run:
64+
@echo "▶️ Running experiment from: $(FILE)"
65+
@docker compose exec app poetry run agentsim run-experiment $(FILE)
66+
67+
## run-local: Run a single, local simulation for quick testing and debugging.
68+
run-local:
69+
@echo "▶️ Running Local Simulation"
70+
@echo " - Package: $(PACKAGE)"
71+
@echo " - Config: $(CONFIG)"
72+
@echo " - Scenario: $(FILE)"
73+
@echo " - Steps: $(STEPS)"
74+
@docker compose exec app poetry run python -m agent_sim.main \
75+
--package $(PACKAGE) \
76+
--config $(CONFIG) \
77+
--scenario $(FILE) \
78+
--steps $(STEPS)
79+
80+
## make-gif: Creates a GIF from the most recent simulation render.
81+
make-gif:
82+
@echo "🎬 Creating GIF from frames in $(RENDER_DIR)..."
83+
@poetry run python scripts/create_gif.py $(RENDER_DIR) simulation.gif --fps $(FPS)
84+
85+
## cli: Run any 'agentsim' command inside the container.
86+
cli:
87+
@echo "Running CLI command: agentsim $(ARGS)"
88+
@docker compose exec app poetry run agentsim $(ARGS)
89+
90+
## test: Run the full pytest suite inside the container.
91+
test:
92+
@echo "🧪 Running pytest test suite..."
93+
@docker compose exec app pytest
94+
95+
## lint: Run the Ruff linter to check for code style issues.
96+
lint:
97+
@echo "🎨 Linting with Ruff..."
98+
@docker compose exec app ruff check .
99+
100+
## help: Display this help message.
101+
help:
102+
@echo "ARLA Project Makefile"
103+
@echo "---------------------"
104+
@echo "Usage: make <target> [VARIABLE=value]"
105+
@echo ""
106+
@echo "Available Targets:"
107+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
108+
sort | \
109+
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
110+
@echo ""
111+
@echo "Example Local Run:"
112+
@echo " make run-local FILE=path/to/your.json STEPS=100"
113+
@echo ""
114+

‎agent-concurrent/src/agent_concurrent/runners.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ async def run(self, systems: Sequence[SystemProtocol], current_tick: int) -> Non
5353
try:
5454
await system.update(current_tick=current_tick)
5555
except Exception as e:
56-
print(f"ERROR: System '{system!r}' failed during serial update at tick {current_tick}: {e}")
56+
print(
57+
f"ERROR: System '{system!r}' failed during serial update at tick {current_tick}: {e}"
58+
)
5759
# In a serial runner, we might choose to stop or continue.
5860
# For robustness, we'll log and continue.
5961
import traceback
@@ -74,7 +76,10 @@ async def run(self, systems: Sequence[SystemProtocol], current_tick: int) -> Non
7476
Creates a task for each system's update method and runs them concurrently.
7577
"""
7678
print(f"--- Tick {current_tick}: Running {len(systems)} systems concurrently")
77-
tasks = [asyncio.create_task(system.update(current_tick=current_tick)) for system in systems]
79+
tasks = [
80+
asyncio.create_task(system.update(current_tick=current_tick))
81+
for system in systems
82+
]
7883

7984
# Use asyncio.gather with return_exceptions=True to ensure that one
8085
# failing system does not stop the others.
@@ -83,7 +88,9 @@ async def run(self, systems: Sequence[SystemProtocol], current_tick: int) -> Non
8388
# After all tasks have completed, check for and log any exceptions.
8489
for result, system in zip(results, systems):
8590
if isinstance(result, Exception):
86-
print(f"ERROR: System '{system!r}' failed during concurrent update at tick {current_tick}: {result}")
91+
print(
92+
f"ERROR: System '{system!r}' failed during concurrent update at tick {current_tick}: {result}"
93+
)
8794
import traceback
8895

8996
traceback.print_exception(type(result), result, result.__traceback__)

‎agent-core/src/agent_core/agents/actions/action_registry.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,18 @@ def load_actions_from_paths(self, module_paths: List[str]) -> None:
3737
importlib.import_module(path)
3838
print(f" Successfully loaded action module: {path}")
3939
except ImportError as e:
40-
print(f"WARNING: Could not import action module at '{path}'. Error: {e}")
40+
print(
41+
f"WARNING: Could not import action module at '{path}'. Error: {e}"
42+
)
4143

4244
def register(self, action_class: Type[ActionInterface]) -> Type[ActionInterface]:
4345
"""
4446
A decorator to register any class that implements the ActionInterface.
4547
"""
4648
if not issubclass(action_class, ActionInterface):
47-
raise TypeError(f"Class {action_class.__name__} must implement ActionInterface to be registered.")
49+
raise TypeError(
50+
f"Class {action_class.__name__} must implement ActionInterface to be registered."
51+
)
4852

4953
try:
5054
instance = action_class()
@@ -56,7 +60,9 @@ def register(self, action_class: Type[ActionInterface]) -> Type[ActionInterface]
5660
) from e
5761

5862
if not isinstance(action_id, str) or not action_id:
59-
raise TypeError(f"Action class {action_class.__name__} has an invalid 'action_id' property.")
63+
raise TypeError(
64+
f"Action class {action_class.__name__} has an invalid 'action_id' property."
65+
)
6066

6167
if action_id in self._actions:
6268
raise ValueError(f"Action with ID '{action_id}' is already registered.")

‎agent-core/src/agent_core/agents/actions/base_action.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ def execute(
7575
raise NotImplementedError
7676

7777
@abstractmethod
78-
def get_feature_vector(self, entity_id: str, simulation_state: Any, params: Dict[str, Any]) -> List[float]:
78+
def get_feature_vector(
79+
self, entity_id: str, simulation_state: Any, params: Dict[str, Any]
80+
) -> List[float]:
7981
raise NotImplementedError
8082

8183
@staticmethod

‎agent-core/src/agent_core/cognition/ai_models/openai_client.py‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def get_client() -> OpenAI:
2929
embedding_cache: Dict[Tuple[str, str], np.ndarray] = {}
3030

3131

32-
def get_embedding_with_cache(text: str, embedding_dim: int, llm_config: Optional[Any] = None) -> Optional[np.ndarray]:
32+
def get_embedding_with_cache(
33+
text: str, embedding_dim: int, llm_config: Optional[Any] = None
34+
) -> Optional[np.ndarray]:
3335
"""Gets an embedding for a single text string, using a cache to avoid repeat API calls."""
3436
# Use direct attribute access on the Pydantic model
3537
model_name = llm_config.embedding_model if llm_config else "text-embedding-ada-002"
@@ -50,14 +52,18 @@ class EmbeddingValidationError(Exception):
5052
"""Raised when embedding validation fails."""
5153

5254

53-
def validate_embedding(embedding: Any, expected_dim: int, name: str = "embedding") -> bool:
55+
def validate_embedding(
56+
embedding: Any, expected_dim: int, name: str = "embedding"
57+
) -> bool:
5458
"""Validate embedding dimensions and properties."""
5559
if embedding is None:
5660
raise EmbeddingValidationError(f"{name} is None")
5761
if not isinstance(embedding, np.ndarray):
5862
raise EmbeddingValidationError(f"{name} is not a numpy array")
5963
if embedding.shape[0] != expected_dim:
60-
raise EmbeddingValidationError(f"{name} dimension mismatch: got {embedding.shape[0]}, expected {expected_dim}")
64+
raise EmbeddingValidationError(
65+
f"{name} dimension mismatch: got {embedding.shape[0]}, expected {expected_dim}"
66+
)
6167
if np.any(np.isnan(embedding)):
6268
raise EmbeddingValidationError(f"{name} contains NaN values")
6369
if np.any(np.isinf(embedding)):
@@ -78,7 +84,9 @@ def get_embedding_from_llm(
7884
try:
7985
response = client.embeddings.create(input=text, model=model_name)
8086
embedding = np.array(response.data[0].embedding).astype(np.float32)
81-
validate_embedding(embedding, expected_embedding_dim, f"embedding for '{text[:50]}...'")
87+
validate_embedding(
88+
embedding, expected_embedding_dim, f"embedding for '{text[:50]}...'"
89+
)
8290
return embedding
8391
except EmbeddingValidationError:
8492
raise
@@ -87,7 +95,9 @@ def get_embedding_from_llm(
8795
return None
8896

8997

90-
def get_embeddings_from_llm_batch(texts: List[str], llm_config: Optional[Any] = None) -> Optional[List[np.ndarray]]:
98+
def get_embeddings_from_llm_batch(
99+
texts: List[str], llm_config: Optional[Any] = None
100+
) -> Optional[List[np.ndarray]]:
91101
"""Gets embeddings for a list of texts in a single, efficient API call."""
92102
if not texts:
93103
return []
@@ -104,15 +114,19 @@ def get_embeddings_from_llm_batch(texts: List[str], llm_config: Optional[Any] =
104114
return None
105115

106116

107-
def query_llm(prompt_text: str, llm_config: Optional[Any] = None) -> tuple[str, int, float]:
117+
def query_llm(
118+
prompt_text: str, llm_config: Optional[Any] = None
119+
) -> tuple[str, int, float]:
108120
"""Queries the LLM, returning the response, token usage, and estimated cost."""
109121
client = get_client()
110122

111123
# Use direct attribute access on the Pydantic model, providing defaults if it's None
112124
model_name = llm_config.completion_model if llm_config else "gpt-4.1-nano"
113125
temp = llm_config.temperature if llm_config else 0.1
114126
max_tok = llm_config.max_tokens if llm_config else 700
115-
prompt_prefix = llm_config.reflection_prompt_prefix if llm_config else "In 300 words or less, "
127+
prompt_prefix = (
128+
llm_config.reflection_prompt_prefix if llm_config else "In 300 words or less, "
129+
)
116130

117131
pricing = {
118132
"gpt-4o-mini": {"prompt": 0.15 / 1_000_000, "completion": 0.60 / 1_000_000},
@@ -129,15 +143,21 @@ def query_llm(prompt_text: str, llm_config: Optional[Any] = None) -> tuple[str,
129143
)
130144

131145
response_text = ""
132-
if response.choices and response.choices[0].message and response.choices[0].message.content:
146+
if (
147+
response.choices
148+
and response.choices[0].message
149+
and response.choices[0].message.content
150+
):
133151
response_text = response.choices[0].message.content.strip()
134152

135153
usage = response.usage
136154
if usage:
137155
prompt_tokens = usage.prompt_tokens
138156
completion_tokens = usage.completion_tokens
139157
total_tokens = usage.total_tokens
140-
cost = (prompt_tokens * model_pricing["prompt"]) + (completion_tokens * model_pricing["completion"])
158+
cost = (prompt_tokens * model_pricing["prompt"]) + (
159+
completion_tokens * model_pricing["completion"]
160+
)
141161
return response_text, total_tokens, cost
142162
else:
143163
return response_text, 0, 0.0

0 commit comments

Comments
 (0)