-
Notifications
You must be signed in to change notification settings - Fork 99
Expand file tree
/
Copy pathcoding_agent_from_scratch_lecture.py
More file actions
198 lines (177 loc) · 6.25 KB
/
coding_agent_from_scratch_lecture.py
File metadata and controls
198 lines (177 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import inspect
import json
import os
from openai import OpenAI
from dotenv import load_dotenv
from pathlib import Path
from typing import Any, Dict, List, Tuple
load_dotenv()
openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
SYSTEM_PROMPT = """
You are a coding assistant whose goal it is to help us solve coding tasks.
You have access to a series of tools you can execute. Hear are the tools you can execute:
{tool_list_repr}
When you want to use a tool, reply with exactly one line in the format: 'tool: TOOL_NAME({{JSON_ARGS}})' and nothing else.
Use compact single-line JSON with double quotes. After receiving a tool_result(...) message, continue the task.
If no tool is needed, respond normally.
"""
YOU_COLOR = "\u001b[94m"
ASSISTANT_COLOR = "\u001b[93m"
RESET_COLOR = "\u001b[0m"
def resolve_abs_path(path_str: str) -> Path:
"""
file.py -> /Users/home/mihail/modern-software-dev-lectures/file.py
"""
path = Path(path_str).expanduser()
if not path.is_absolute():
path = (Path.cwd() / path).resolve()
return path
def read_file_tool(filename: str) -> Dict[str, Any]:
"""
Gets the full content of a file provided by the user.
:param filename: The name of the file to read.
:return: The full content of the file.
"""
full_path = resolve_abs_path(filename)
print(full_path)
with open(str(full_path), "r") as f:
content = f.read()
return {
"file_path": str(full_path),
"content": content
}
def list_files_tool(path: str) -> Dict[str, Any]:
"""
Lists the files in a directory provided by the user.
:param path: The path to a directory to list files from.
:return: A list of files in the directory.
"""
full_path = resolve_abs_path(path)
all_files = []
for item in full_path.iterdir():
all_files.append({
"filename": item.name,
"type": "file" if item.is_file() else "dir"
})
return {
"path": str(full_path),
"files": all_files
}
def edit_file_tool(path: str, old_str: str, new_str: str) -> Dict[str, Any]:
"""
Replaces first occurrence of old_str with new_str in file. If old_str is empty,
create/overwrite file with new_str.
:param path: The path to the file to edit.
:param old_str: The string to replace.
:param new_str: The string to replace with.
:return: A dictionary with the path to the file and the action taken.
"""
full_path = resolve_abs_path(path)
if old_str == "":
full_path.write_text(new_str, encoding="utf-8")
return {
"path": str(full_path),
"action": "created_file"
}
original = full_path.read_text(encoding="utf-8")
if original.find(old_str) == -1:
return {
"path": str(full_path),
"action": "old_str not found"
}
edited = original.replace(old_str, new_str, 1)
full_path.write_text(edited, encoding="utf-8")
return {
"path": str(full_path),
"action": "edited"
}
TOOL_REGISTRY = {
"read_file": read_file_tool,
"list_files": list_files_tool,
"edit_file": edit_file_tool
}
def get_tool_str_representation(tool_name: str) -> str:
tool = TOOL_REGISTRY[tool_name]
return f"""
Name: {tool_name}
Description: {tool.__doc__}
Signature: {inspect.signature(tool)}
"""
def get_full_system_prompt():
tool_str_repr = ""
for tool_name in TOOL_REGISTRY:
tool_str_repr += "TOOL\n===" + get_tool_str_representation(tool_name)
tool_str_repr += f"\n{"="*15}\n"
return SYSTEM_PROMPT.format(tool_list_repr=tool_str_repr)
def extract_tool_invocations(text: str) -> List[Tuple[str, Dict[str, Any]]]:
"""
Return list of (tool_name, args) requested in 'tool: name({...})' lines.
The parser expects single-line, compact JSON in parentheses.
"""
invocations = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line.startswith("tool:"):
continue
try:
after = line[len("tool:"):].strip()
name, rest = after.split("(", 1)
name = name.strip()
if not rest.endswith(")"):
continue
json_str = rest[:-1].strip()
args = json.loads(json_str)
invocations.append((name, args))
except Exception:
continue
return invocations
def execute_llm_call(conversation: List[Dict[str, str]]):
response = openai_client.chat.completions.create(
model="gpt-5",
messages=conversation,
max_completion_tokens=2000
)
return response.choices[0].message.content
def run_coding_agent_loop():
print(get_full_system_prompt())
conversation = [{
"role": "system",
"content": get_full_system_prompt()
}]
while True:
try:
user_input = input(f"{YOU_COLOR}You:{RESET_COLOR}:")
except (KeyboardInterrupt, EOFError):
break
conversation.append({
"role": "user",
"content": user_input.strip()
})
while True:
assistant_response = execute_llm_call(conversation)
tool_invocations = extract_tool_invocations(assistant_response)
if not tool_invocations:
print(f"{ASSISTANT_COLOR}Assistant:{RESET_COLOR}: {assistant_response}")
conversation.append({
"role": "assistant",
"content": assistant_response
})
break
for name, args in tool_invocations:
tool = TOOL_REGISTRY[name]
resp = ""
print(name, args)
if name == "read_file":
resp = tool(args.get("filename", "."))
elif name == "list_files":
resp = tool(args.get("path", "."))
elif name == "edit_file":
resp = tool(args.get("path", "."),
args.get("old_str", ""),
args.get("new_str", ""))
conversation.append({
"role": "user",
"content": f"tool_result({json.dumps(resp)})"
})
if __name__ == "__main__":
run_coding_agent_loop()