Skip to content

Commit 2e57759

Browse files
authored
Merge pull request #2 from wandb/git-state-tracking
Git state tracking and restore.
2 parents 392428a + e850ad3 commit 2e57759

File tree

11 files changed

+597
-116
lines changed

11 files changed

+597
-116
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: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Optional, Union
22
from pydantic import Field
33
from openai import OpenAI
44
from openai._types import NotGiven
@@ -7,15 +7,37 @@
77
)
88

99
import weave
10+
from weave.trace.vals import WeaveList
1011
from weave.flow.chat_util import OpenAIStream
1112

1213
from .console import Console
1314
from .tool_calling import chat_call_tool_params, perform_tool_calls
15+
from .environment import get_current_environment, EnvironmentSnapshotKey
16+
17+
18+
def get_last_assistant_content(history: list[Any]) -> Optional[str]:
19+
for i in range(len(history) - 1, -1, -1):
20+
if history[i]["role"] == "assistant" and "content" in history[i]:
21+
return history[i]["content"]
22+
elif history[i]["role"] == "user":
23+
break
24+
return None
25+
26+
27+
# Weave bug workaround: adding two WeaveLists can create that cause
28+
# downstream crashes.
29+
def weavelist_add(self: Union[list, WeaveList], other: list) -> Union[list, WeaveList]:
30+
if isinstance(self, list):
31+
return self + other
32+
if not isinstance(other, list):
33+
return NotImplemented
34+
return WeaveList(list(self) + other, server=self.server)
1435

1536

1637
class AgentState(weave.Object):
1738
# The chat message history.
1839
history: list[Any] = Field(default_factory=list)
40+
env_snapshot_key: Optional[EnvironmentSnapshotKey] = None
1941

2042

2143
class Agent(weave.Object):
@@ -80,7 +102,18 @@ def step(self, state: AgentState) -> AgentState:
80102
perform_tool_calls(self.tools, response_message.tool_calls)
81103
)
82104

83-
return AgentState(history=state.history + new_messages)
105+
# new_history = state.history + new_messages
106+
new_history = weavelist_add(state.history, new_messages)
107+
last_assistant_message = get_last_assistant_content(new_history)
108+
if last_assistant_message:
109+
message = last_assistant_message
110+
else:
111+
message = "commit"
112+
113+
environment = get_current_environment()
114+
snapshot_key = environment.make_snapshot(message)
115+
116+
return AgentState(history=new_history, env_snapshot_key=snapshot_key)
84117

85118
@weave.op()
86119
def run(self, state: AgentState):

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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from dataclasses import dataclass
2+
from typing import Protocol
3+
from contextvars import ContextVar
4+
from contextlib import contextmanager
5+
6+
7+
from .git import GitRepo
8+
9+
10+
@dataclass
11+
class EnvironmentSnapshotKey:
12+
env_id: str
13+
snapshot_info: dict
14+
15+
16+
class Environment(Protocol):
17+
def start_session(self, session_id: str): ...
18+
19+
def finish_session(self): ...
20+
21+
def make_snapshot(self, message: str) -> EnvironmentSnapshotKey: ...
22+
23+
@classmethod
24+
def restore_from_snapshot_key(cls, ref: EnvironmentSnapshotKey): ...
25+
26+
27+
@contextmanager
28+
def environment_session(env: Environment, session_id: str):
29+
env.start_session(session_id)
30+
token = environment_context.set(env)
31+
try:
32+
yield env
33+
finally:
34+
env.finish_session()
35+
environment_context.reset(token)
36+
37+
38+
def get_current_environment() -> Environment:
39+
return environment_context.get()
40+
41+
42+
class GitEnvironment(Environment):
43+
def __init__(self, repo: GitRepo):
44+
self.repo = repo
45+
self.original_git_ref = None
46+
self.programmer_branch = None
47+
48+
def start_session(self, session_id: str):
49+
self.original_git_ref = self.repo.get_current_head()
50+
self.programmer_branch = f"programmer-{session_id}"
51+
print("programmer_branch:", self.programmer_branch)
52+
self.repo.checkout_new(self.programmer_branch)
53+
54+
def finish_session(self):
55+
if self.original_git_ref is None or self.programmer_branch is None:
56+
raise ValueError("Session not started")
57+
self.repo.checkout_and_copy(self.original_git_ref)
58+
59+
def make_snapshot(self, message: str) -> EnvironmentSnapshotKey:
60+
commit_hash = self.repo.add_all_and_commit(message)
61+
return EnvironmentSnapshotKey(
62+
"git", {"origin": self.repo.get_origin_url(), "commit": commit_hash}
63+
)
64+
65+
@classmethod
66+
def restore_from_snapshot_key(cls, ref: EnvironmentSnapshotKey):
67+
origin = ref.snapshot_info["origin"]
68+
commit = ref.snapshot_info["commit"]
69+
repo = GitRepo.from_current_dir()
70+
if not repo:
71+
raise ValueError("No git repo found")
72+
if origin != repo.get_origin_url():
73+
raise ValueError("Origin URL mismatch")
74+
repo.checkout_existing(commit)
75+
print("Checked out commit", commit)
76+
return cls(repo)
77+
78+
79+
class NoopEnvironment(Environment):
80+
def start_session(self, session_id: str):
81+
pass
82+
83+
def finish_session(self):
84+
pass
85+
86+
def make_snapshot(self, message: str):
87+
pass
88+
89+
@classmethod
90+
def restore_from_snapshot_key(cls, ref: str):
91+
pass
92+
93+
94+
def restore_environment(snapshot_key: EnvironmentSnapshotKey) -> Environment:
95+
if snapshot_key.env_id == "git":
96+
return GitEnvironment.restore_from_snapshot_key(snapshot_key)
97+
return NoopEnvironment()
98+
99+
100+
environment_context: ContextVar[Environment] = ContextVar(
101+
"environment", default=NoopEnvironment()
102+
)

0 commit comments

Comments
 (0)