Skip to content

Commit

Permalink
Store .programmer directory and weave.db at git root. (#12)
Browse files Browse the repository at this point in the history
* Load settings from git root

* Update README
  • Loading branch information
shawnlewis authored Aug 23, 2024
1 parent 4407266 commit 441fd93
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 43 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,15 @@ To resume from an earlier state:
programmer --state <state_ref>
```

## Settings
## Tracking

programmer settings set weave_logging <value>
- off: no logging
- local: log to local sqlite db
- cloudd: log to weave cloud at wandb.ai
Programmer is designed to get better over time. For that we need to track trajectories, identify good and bad ones to add to Evaluations (like unit tests for AI), and then iterate on programmer's prompts and architecture to improve against the Evaluations.

programmer settings set git_tracking <value>
- off: no git tracking
- on: programmer with make programmer-* branches and track changes
By default all trajectories are logged to `.programmer/weave.db`. You can turn on cloud logging with `programmer settings set weave_logging cloud`. Trajectories will be saved to Weave at wandb.ai

## UI
You can turn on git tracking with `programmer settings set git_tracking on` to get programmer to track all of its work in "programmer-*" branches. Each git state will be associated with the Weave trajectories, and you can browse the diffs with `programmer ui`

When weave_logging is set to "cloud" you can use the Weave UI at wandb.ai to browse traces.
## UI

Run

Expand All @@ -62,6 +57,24 @@ programmer ui

to run the local streamlit UI. This should work with either weave_logging:cloud or weave_logging:local, but there are some bugs with local mode at the moment.

![Programmer UI screenshot](./assets/programmer-ui.png)

# Weave UI

When weave_logging is set to "cloud" you can use the Weave UI at wandb.ai to browse traces.

## Settings

Settings are stored in .programmer/settings

programmer settings set weave_logging <value>
- off: no logging
- local: log to local sqlite db
- cloudd: log to weave cloud at wandb.ai

programmer settings set git_tracking <value>
- off: no git tracking
- on: programmer with make programmer-* branches and track changes

## Improving programmer

Expand All @@ -80,7 +93,8 @@ python evaluate.py

## roadmap

- [ ] weave server tracking
- [ ] git state tracking
- [x] weave server tracking
- [x] git state tracking
- [x] basic trajectory UI
- [ ] user-annotation of good and bad behaviors
- [ ] eval generation
Binary file added assets/programmer-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion programmer/programmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def main():
curdir = os.path.basename(os.path.abspath(os.curdir))
weave.init(f"programmer-{curdir}")
elif logging_mode == "local":
init_local_client()
init_local_client(os.path.join(SettingsManager.PROGRAMMER_DIR, "weave.db"))

args = parser.parse_args()

Expand Down
83 changes: 60 additions & 23 deletions programmer/settings_manager.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import os


class SettingsError(Exception):
pass


class SettingsManager:
SETTINGS_DIR = ".programmer"
PROGRAMMER_DIR = ".programmer"
SETTINGS_FILE = "settings"
DEFAULT_SETTINGS = {
"weave_logging": "local",
"git_tracking": "off"
}
DEFAULT_SETTINGS = {"weave_logging": "local", "git_tracking": "off"}
ALLOWED_VALUES = {
"weave_logging": ["off", "local", "cloud"],
"git_tracking": ["off", "on"]
"git_tracking": ["off", "on"],
}

@classmethod
def set_settings_dir(cls, dir_path):
cls.SETTINGS_DIR = dir_path
cls.PROGRAMMER_DIR = dir_path

@staticmethod
def initialize_settings():
"""
Ensure that the settings directory and file exist, and populate missing settings with defaults.
"""
if not os.path.exists(SettingsManager.SETTINGS_DIR):
os.makedirs(SettingsManager.SETTINGS_DIR)
settings_path = os.path.join(SettingsManager.SETTINGS_DIR, SettingsManager.SETTINGS_FILE)
# Import GitRepo from git module
from .git import GitRepo

# Check if we're in a Git repository
settings_dir = None
git_repo = GitRepo.from_current_dir()
if git_repo:
# If in a Git repo, set the settings directory to the repo root
repo_root = git_repo.repo.working_tree_dir
if repo_root:
settings_dir = os.path.join(repo_root, SettingsManager.PROGRAMMER_DIR)
if not settings_dir:
# use abs path
settings_dir = os.path.abspath(SettingsManager.PROGRAMMER_DIR)

SettingsManager.PROGRAMMER_DIR = settings_dir

if not os.path.exists(SettingsManager.PROGRAMMER_DIR):
os.makedirs(SettingsManager.PROGRAMMER_DIR)
settings_path = os.path.join(
SettingsManager.PROGRAMMER_DIR, SettingsManager.SETTINGS_FILE
)
if not os.path.exists(settings_path):
SettingsManager.write_default_settings()
else:
Expand All @@ -38,20 +55,29 @@ def validate_and_complete_settings():
"""
Validate the settings file format and complete it with default values if necessary.
"""
settings_path = os.path.join(SettingsManager.SETTINGS_DIR, SettingsManager.SETTINGS_FILE)
settings_path = os.path.join(
SettingsManager.PROGRAMMER_DIR, SettingsManager.SETTINGS_FILE
)
with open(settings_path, "r") as f:
lines = f.readlines()

settings = {}
for line in lines:
if "=" not in line:
raise SettingsError(f"Malformed settings line: '{line.strip()}'.\n"
f"Please ensure each setting is in 'key=value' format.\n"
f"Settings file location: {settings_path}")
raise SettingsError(
f"Malformed settings line: '{line.strip()}'.\n"
f"Please ensure each setting is in 'key=value' format.\n"
f"Settings file location: {settings_path}"
)
key, value = line.strip().split("=", 1)
if key in SettingsManager.ALLOWED_VALUES and value not in SettingsManager.ALLOWED_VALUES[key]:
raise SettingsError(f"Invalid value '{value}' for setting '{key}'. Allowed values are: {SettingsManager.ALLOWED_VALUES[key]}\n"
f"Settings file location: {settings_path}")
if (
key in SettingsManager.ALLOWED_VALUES
and value not in SettingsManager.ALLOWED_VALUES[key]
):
raise SettingsError(
f"Invalid value '{value}' for setting '{key}'. Allowed values are: {SettingsManager.ALLOWED_VALUES[key]}\n"
f"Settings file location: {settings_path}"
)
settings[key] = value

# Add missing default settings
Expand All @@ -69,7 +95,9 @@ def write_default_settings():
"""
Write the default settings to the settings file.
"""
settings_path = os.path.join(SettingsManager.SETTINGS_DIR, SettingsManager.SETTINGS_FILE)
settings_path = os.path.join(
SettingsManager.PROGRAMMER_DIR, SettingsManager.SETTINGS_FILE
)
with open(settings_path, "w") as f:
for key, value in SettingsManager.DEFAULT_SETTINGS.items():
f.write(f"{key}={value}\n")
Expand All @@ -79,7 +107,9 @@ def get_setting(key):
"""
Retrieve a setting's value by key.
"""
settings_path = os.path.join(SettingsManager.SETTINGS_DIR, SettingsManager.SETTINGS_FILE)
settings_path = os.path.join(
SettingsManager.PROGRAMMER_DIR, SettingsManager.SETTINGS_FILE
)
if not os.path.exists(settings_path):
return None
with open(settings_path, "r") as f:
Expand All @@ -93,10 +123,17 @@ def set_setting(key, value):
"""
Set a setting's value by key, validating allowed values.
"""
settings_path = os.path.join(SettingsManager.SETTINGS_DIR, SettingsManager.SETTINGS_FILE)
if key in SettingsManager.ALLOWED_VALUES and value not in SettingsManager.ALLOWED_VALUES[key]:
raise SettingsError(f"Invalid value '{value}' for setting '{key}'. Allowed values are: {SettingsManager.ALLOWED_VALUES[key]}\n"
f"Settings file location: {settings_path}")
settings_path = os.path.join(
SettingsManager.PROGRAMMER_DIR, SettingsManager.SETTINGS_FILE
)
if (
key in SettingsManager.ALLOWED_VALUES
and value not in SettingsManager.ALLOWED_VALUES[key]
):
raise SettingsError(
f"Invalid value '{value}' for setting '{key}'. Allowed values are: {SettingsManager.ALLOWED_VALUES[key]}\n"
f"Settings file location: {settings_path}"
)

lines = []
found = False
Expand Down
15 changes: 11 additions & 4 deletions programmer/tests/test_settings_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import tempfile
from programmer.settings_manager import SettingsManager, SettingsError


@pytest.fixture(scope="function")
def setup_and_teardown_settings():
"""Fixture to set up and tear down a temporary settings directory for testing."""
with tempfile.TemporaryDirectory() as temp_dir:
original_settings_dir = SettingsManager.SETTINGS_DIR
original_settings_dir = SettingsManager.PROGRAMMER_DIR
SettingsManager.set_settings_dir(temp_dir)
test_file = os.path.join(temp_dir, SettingsManager.SETTINGS_FILE)
try:
Expand All @@ -23,7 +24,9 @@ def test_initialize_settings_creates_file_with_defaults(setup_and_teardown_setti
assert os.path.exists(test_file)
with open(test_file, "r") as f:
settings = f.read().strip()
expected_settings = "\n".join(f"{key}={value}" for key, value in SettingsManager.DEFAULT_SETTINGS.items())
expected_settings = "\n".join(
f"{key}={value}" for key, value in SettingsManager.DEFAULT_SETTINGS.items()
)
assert settings == expected_settings


Expand All @@ -45,14 +48,18 @@ def test_set_setting_adds_new(setup_and_teardown_settings):
assert SettingsManager.get_setting("new_setting") == "value"


def test_validate_and_complete_settings_raises_error_on_malformed_line(setup_and_teardown_settings):
def test_validate_and_complete_settings_raises_error_on_malformed_line(
setup_and_teardown_settings,
):
with open(setup_and_teardown_settings, "w") as f:
f.write("malformed_line\n")
with pytest.raises(SettingsError):
SettingsManager.validate_and_complete_settings()


def test_validate_and_complete_settings_adds_missing_defaults(setup_and_teardown_settings):
def test_validate_and_complete_settings_adds_missing_defaults(
setup_and_teardown_settings,
):
with open(setup_and_teardown_settings, "w") as f:
f.write("weave_logging=local\n") # Missing git_tracking
SettingsManager.validate_and_complete_settings()
Expand Down
15 changes: 12 additions & 3 deletions ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@


@st.cache_resource
def init_local_weave():
return init_local_client()
def init_local_weave(db_path: str = "weave.db"):
return init_local_client(db_path)


@st.cache_resource
Expand All @@ -28,6 +28,7 @@ def init_remote_weave(project: str):


def init_from_settings() -> WeaveClient:
SettingsManager.initialize_settings()
weave_logging_setting = SettingsManager.get_setting("weave_logging")
if weave_logging_setting == "off":
st.error(
Expand All @@ -36,7 +37,9 @@ def init_from_settings() -> WeaveClient:
st.stop()
raise Exception("Should never get here")
elif weave_logging_setting == "local":
return init_local_weave()
return init_local_weave(
os.path.join(SettingsManager.PROGRAMMER_DIR, "weave.db")
)
elif weave_logging_setting == "cloud":
curdir = os.path.basename(os.path.abspath(os.curdir))
return init_remote_weave(f"programmer-{curdir}")
Expand Down Expand Up @@ -165,12 +168,18 @@ def print_session_call(session_id):


session_calls_df = cached_calls(client, "session", expand_refs=["inputs.agent_state"])
if len(session_calls_df) == 0:
st.error("No programmer sessions found.")
st.stop()
session_user_message_df = session_calls_df["inputs.agent_state.history"].apply(
lambda v: v[-1]["content"]
)


with st.sidebar:
if st.button("Refresh"):
st.cache_data.clear()
st.rerun()
message_ids = {
f"{cid[-5:]}: {m}": cid
for cid, m in reversed(
Expand Down

0 comments on commit 441fd93

Please sign in to comment.