Skip to content

Commit e850ad3

Browse files
committed
Add settings manager.
1 parent ecdfa01 commit e850ad3

File tree

9 files changed

+348
-18
lines changed

9 files changed

+348
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ programmer.egg-info
55

66
# PyPI uploads
77
.pypirc
8+
.programmer

docs/design/settings_management.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Settings Management Feature Design
2+
3+
Author: programmer
4+
5+
## Introduction
6+
7+
This document outlines the design for the settings management feature to be implemented in the 'programmer' project. The feature will allow users to manage settings related to weave logging and git tracking. These settings should persist across sessions and be stored in the user’s current directory.
8+
9+
## Feature Overview
10+
11+
The settings management feature will provide the following functionalities:
12+
13+
1. **Weave Logging Control**: Users can control the state of weave logging with three options:
14+
- Off
15+
- Local
16+
- Cloud
17+
18+
2. **Git Tracking Control**: Users can control the state of git tracking with two options:
19+
- Off
20+
- On
21+
22+
## Requirements
23+
24+
- The settings should persist across sessions.
25+
- The settings should be stored in a file located in the user’s current directory.
26+
- The feature should provide an easy interface for users to change settings.
27+
28+
## Design Details
29+
30+
### Settings Storage
31+
32+
- The settings will be stored in a directory named `.programmer` in the user's current directory.
33+
- Within this directory, settings will be saved in a file named `settings`.
34+
- The file will use a simple key-value format for storing settings:
35+
36+
```
37+
weave_logging=off
38+
git_tracking=on
39+
```
40+
41+
### Interface
42+
43+
- A command-line interface will be provided to change settings. Users will be able to run commands such as:
44+
45+
```
46+
programmer settings set weave_logging local
47+
programmer settings get weave_logging
48+
```
49+
50+
### Implementation Steps
51+
52+
1. **Create Settings Directory and File Structure**: Define the structure and location of the settings file within the `.programmer` directory.
53+
2. **Implement CLI for Settings Management**: Develop commands to get and set the settings.
54+
3. **Persist Settings Across Sessions**: Ensure that changes to settings are saved to the file and reloaded when the application starts.
55+
56+
## Conclusion
57+
58+
This design document provides a comprehensive overview of the settings management feature for the 'programmer' project. By following this design, we aim to implement a robust settings management system that allows users to control weave logging and git tracking effectively.

docs/dev/devlog-shawn.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
8/17/24
2+
-------
3+
4+
Git tracking is working in a branch now. Here's what it does:
5+
- If you're in a git repo, programmer automatically creates branches while it works.
6+
- The git state is stored in the trajectories that are auto-saved to Weave.
7+
- This means you can roll back programmer to any prior point, and both the conversation, and git repo state will be restored.
8+
9+
Why do this? To improve an AI application like programmer, you need to experiment.
10+
11+
Let's use an example. Suppose you're using programmer, you ask it to run some unit tests, and programmer say something like "OK here's a plan, I'll run the `pytest` command, would you like to proceed?"
12+
13+
This is annoying, we just want it to run the command instead of stopping and asking the user.
14+
15+
We can try to fix this with prompt engineering. We want to experiment with a bunch of different prompts, starting from the prior state of conversation and file system.
16+
17+
18+
OK, above is the beginning of a write up of how to talk about this feature...
19+
20+
Now I want to do a few things:
21+
- think about if the git feature is ready.
22+
- write a new feature using programmer: programmer settings controls.
23+
24+
Bug:
25+
- programmer fails to restore my original branch

programmer/agent.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Optional
1+
from typing import Any, Optional, Union
22
from pydantic import Field
33
from openai import OpenAI
44
from openai._types import NotGiven
@@ -26,7 +26,9 @@ def get_last_assistant_content(history: list[Any]) -> Optional[str]:
2626

2727
# Weave bug workaround: adding two WeaveLists can create that cause
2828
# downstream crashes.
29-
def weavelist_add(self: WeaveList, other: list) -> WeaveList:
29+
def weavelist_add(self: Union[list, WeaveList], other: list) -> Union[list, WeaveList]:
30+
if isinstance(self, list):
31+
return self + other
3032
if not isinstance(other, list):
3133
return NotImplemented
3234
return WeaveList(list(self) + other, server=self.server)

programmer/console.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import sys
12
from rich.console import Console as RichConsole
23
from rich.padding import Padding
34

5+
# Adjusting import to absolute path
6+
from .settings_manager import SettingsManager
7+
48
console = RichConsole()
59

610

@@ -46,3 +50,32 @@ def tool_call_complete(tool_response: str) -> None:
4650
@staticmethod
4751
def user_input_complete(user_input: str) -> None:
4852
console.print()
53+
54+
@staticmethod
55+
def settings_command(command_args):
56+
if len(command_args) < 2:
57+
console.print("[red]Invalid settings command[/red]")
58+
return
59+
action = command_args[0]
60+
key = command_args[1]
61+
if action == "get":
62+
value = SettingsManager.get_setting(key)
63+
if value is not None:
64+
console.print(f"{key} = {value}")
65+
else:
66+
console.print(f"[red]Setting '{key}' not found[/red]")
67+
elif action == "set" and len(command_args) == 3:
68+
value = command_args[2]
69+
SettingsManager.set_setting(key, value)
70+
console.print(f"[green]Setting '{key}' updated to '{value}'[/green]")
71+
else:
72+
console.print("[red]Invalid settings command[/red]")
73+
74+
75+
# Example of integrating a basic command line argument parsing
76+
if __name__ == "__main__":
77+
SettingsManager.initialize_settings()
78+
if len(sys.argv) > 1 and sys.argv[1] == "settings":
79+
Console.settings_command(sys.argv[2:])
80+
else:
81+
Console.welcome()

programmer/environment.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,6 @@ def restore_from_snapshot_key(cls, ref: str):
9191
pass
9292

9393

94-
def init_environment() -> Environment:
95-
git_repo = GitRepo.from_current_dir()
96-
if git_repo:
97-
return GitEnvironment(git_repo)
98-
return NoopEnvironment()
99-
100-
10194
def restore_environment(snapshot_key: EnvironmentSnapshotKey) -> Environment:
10295
if snapshot_key.env_id == "git":
10396
return GitEnvironment.restore_from_snapshot_key(snapshot_key)

programmer/programmer.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
from .agent import AgentState
1111
from .console import Console
1212
from .config import agent
13+
from .environment import (
14+
environment_session,
15+
restore_environment,
16+
GitEnvironment,
17+
NoopEnvironment,
18+
)
19+
from .settings_manager import SettingsManager
1320

14-
from .environment import init_environment, environment_session, restore_environment
21+
from .git import GitRepo
1522

1623

1724
@weave.op
@@ -40,31 +47,64 @@ def user_input_step(state: AgentState) -> AgentState:
4047
@weave.op
4148
def session(agent_state: AgentState):
4249
call = weave.get_current_call()
43-
if call is None or call.id is None:
44-
raise ValueError("unexpected Weave state")
45-
environment = init_environment()
4650

47-
with environment_session(environment, call.id):
51+
session_id = None
52+
if call:
53+
session_id = call.id
54+
55+
git_repo = GitRepo.from_current_dir()
56+
git_tracking_enabled = SettingsManager.get_setting("git_tracking") == "on"
57+
if git_tracking_enabled and git_repo:
58+
env = GitEnvironment(git_repo)
59+
else:
60+
env = NoopEnvironment()
61+
62+
with environment_session(env, session_id):
4863
while True:
4964
agent_state = agent.run(agent_state)
5065
agent_state = user_input_step(agent_state)
5166

5267

5368
def main():
5469
parser = argparse.ArgumentParser(description="Programmer")
70+
subparsers = parser.add_subparsers(dest="command")
71+
72+
# Subparser for the settings command
73+
settings_parser = subparsers.add_parser("settings", help="Manage settings")
74+
settings_parser.add_argument(
75+
"action", choices=["get", "set"], help="Action to perform"
76+
)
77+
settings_parser.add_argument("key", help="The setting key")
78+
settings_parser.add_argument("value", nargs="?", help="The value to set")
79+
5580
parser.add_argument(
5681
"--state", type=str, help="weave ref of the state to begin from"
5782
)
5883

59-
curdir = os.path.basename(os.path.abspath(os.curdir))
84+
SettingsManager.initialize_settings()
85+
86+
# Initialize settings
87+
88+
args = parser.parse_args()
89+
90+
if args.command == "settings":
91+
Console.settings_command(
92+
[args.action, args.key, args.value]
93+
if args.value
94+
else [args.action, args.key]
95+
)
96+
return
6097

6198
# log to local sqlite db for now
62-
# weave.init(f"programmerdev1-{curdir}")
63-
weave.init_local_client()
99+
logging_mode = SettingsManager.get_setting("weave_logging")
100+
if logging_mode == "cloud":
101+
curdir = os.path.basename(os.path.abspath(os.curdir))
102+
weave.init(f"programmerdev1-{curdir}")
103+
elif logging_mode == "local":
104+
weave.init_local_client()
64105

65106
Console.welcome()
66107

67-
args, remaining = parser.parse_known_args()
68108
if args.state:
69109
state = weave.ref(args.state).get()
70110
if state.env_snapshot_key:

programmer/settings_manager.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
3+
SETTINGS_DIR = ".programmer"
4+
SETTINGS_FILE = "settings"
5+
DEFAULT_SETTINGS = {
6+
"weave_logging": "local",
7+
"git_tracking": "off"
8+
}
9+
ALLOWED_VALUES = {
10+
"weave_logging": ["off", "local", "cloud"],
11+
"git_tracking": ["off", "on"]
12+
}
13+
14+
15+
class SettingsError(Exception):
16+
pass
17+
18+
19+
class SettingsManager:
20+
@staticmethod
21+
def initialize_settings():
22+
"""
23+
Ensure that the settings directory and file exist, and populate missing settings with defaults.
24+
"""
25+
if not os.path.exists(SETTINGS_DIR):
26+
os.makedirs(SETTINGS_DIR)
27+
settings_path = os.path.join(SETTINGS_DIR, SETTINGS_FILE)
28+
if not os.path.exists(settings_path):
29+
SettingsManager.write_default_settings()
30+
else:
31+
SettingsManager.validate_and_complete_settings()
32+
33+
@staticmethod
34+
def validate_and_complete_settings():
35+
"""
36+
Validate the settings file format and complete it with default values if necessary.
37+
"""
38+
settings_path = os.path.join(SETTINGS_DIR, SETTINGS_FILE)
39+
with open(settings_path, "r") as f:
40+
lines = f.readlines()
41+
42+
settings = {}
43+
for line in lines:
44+
if "=" not in line:
45+
raise SettingsError(f"Malformed settings line: '{line.strip()}'.\n"
46+
f"Please ensure each setting is in 'key=value' format.\n"
47+
f"Settings file location: {settings_path}")
48+
key, value = line.strip().split("=", 1)
49+
if key in ALLOWED_VALUES and value not in ALLOWED_VALUES[key]:
50+
raise SettingsError(f"Invalid value '{value}' for setting '{key}'. Allowed values are: {ALLOWED_VALUES[key]}\n"
51+
f"Settings file location: {settings_path}")
52+
settings[key] = value
53+
54+
# Add missing default settings
55+
for key, default_value in DEFAULT_SETTINGS.items():
56+
if key not in settings:
57+
settings[key] = default_value
58+
59+
# Rewrite the settings file with complete settings
60+
with open(settings_path, "w") as f:
61+
for key, value in settings.items():
62+
f.write(f"{key}={value}\n")
63+
64+
@staticmethod
65+
def write_default_settings():
66+
"""
67+
Write the default settings to the settings file.
68+
"""
69+
settings_path = os.path.join(SETTINGS_DIR, SETTINGS_FILE)
70+
with open(settings_path, "w") as f:
71+
for key, value in DEFAULT_SETTINGS.items():
72+
f.write(f"{key}={value}\n")
73+
74+
@staticmethod
75+
def get_setting(key):
76+
"""
77+
Retrieve a setting's value by key.
78+
"""
79+
settings_path = os.path.join(SETTINGS_DIR, SETTINGS_FILE)
80+
if not os.path.exists(settings_path):
81+
return None
82+
with open(settings_path, "r") as f:
83+
for line in f.readlines():
84+
if line.startswith(key):
85+
return line.split("=")[1].strip()
86+
return None
87+
88+
@staticmethod
89+
def set_setting(key, value):
90+
"""
91+
Set a setting's value by key, validating allowed values.
92+
"""
93+
settings_path = os.path.join(SETTINGS_DIR, SETTINGS_FILE)
94+
if key in ALLOWED_VALUES and value not in ALLOWED_VALUES[key]:
95+
raise SettingsError(f"Invalid value '{value}' for setting '{key}'. Allowed values are: {ALLOWED_VALUES[key]}\n"
96+
f"Settings file location: {settings_path}")
97+
98+
lines = []
99+
found = False
100+
if os.path.exists(settings_path):
101+
with open(settings_path, "r") as f:
102+
lines = f.readlines()
103+
for i, line in enumerate(lines):
104+
if line.startswith(key):
105+
lines[i] = f"{key}={value}\n"
106+
found = True
107+
break
108+
if not found:
109+
lines.append(f"{key}={value}\n")
110+
with open(settings_path, "w") as f:
111+
f.writelines(lines)

0 commit comments

Comments
 (0)