seansullivan commited on
Commit
d2e3e49
·
verified ·
1 Parent(s): 6a7d14b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +352 -0
app.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import streamlit as st
3
+ import requests
4
+ import base64
5
+ import json
6
+ import shutil
7
+ from urllib.parse import urlparse
8
+ from git import Repo
9
+ from git.exc import GitCommandError
10
+ from typing import List, Dict, Any, TypedDict, Annotated
11
+ import operator
12
+ import asyncio
13
+ from langchain.tools import StructuredTool, Tool
14
+ from langchain_core.pydantic_v1 import BaseModel, Field
15
+ from langchain_core.messages import BaseMessage, HumanMessage
16
+ from langchain_anthropic import ChatAnthropic
17
+ from langchain_community.tools import ShellTool
18
+ from langgraph.prebuilt import create_react_agent
19
+
20
+ # Initialize show_system_prompt in session state
21
+ if "show_system_prompt" not in st.session_state:
22
+ st.session_state.show_system_prompt = False
23
+
24
+ # Add a button to toggle the visibility of the system prompt
25
+ if st.button("Show System Prompt" if not st.session_state.show_system_prompt else "Hide System Prompt"):
26
+ st.session_state.show_system_prompt = not st.session_state.show_system_prompt
27
+
28
+ # Show title and description.
29
+ st.title("Coder for NextJS Templates")
30
+ st.write(
31
+ "This chatbot connects to a Next.JS Github Repository to answer questions and modify code "
32
+ "given the user's prompt. Please input your repo url and github token to allow the AI to connect, then query it by asking questions or requesting feature changes!"
33
+ )
34
+
35
+ # Ask user for their Github Repo URL, Github Token, and Anthropic API key via `st.text_input`.
36
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
37
+ os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
38
+ os.environ["LANGCHAIN_PROJECT"] = "Github-Agent"
39
+
40
+ github_repo_url = st.text_input("Github Repo URL (e.g., https://github.com/user/repo)")
41
+
42
+ # Use st.markdown for the hyperlink text
43
+ st.markdown(
44
+ '[How to get your Github Token](https://docs.github.com/en/[email protected]/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)'
45
+ )
46
+ github_token = st.text_input("Enter your Github Token", type="password")
47
+
48
+ # anthropic_api_key = st.text_input("Anthropic API Key", type="password")
49
+
50
+ anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
51
+
52
+ if not (github_repo_url and github_token and anthropic_api_key):
53
+ st.info("Please add your Github Repo URL, Github Token, and Anthropic API key to continue.", icon="🗝️")
54
+ else:
55
+ # Set environment variables
56
+ os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
57
+ os.environ["GITHUB_TOKEN"] = github_token
58
+
59
+ # Parse the repository URL to extract user_name and REPO_NAME
60
+ parsed_url = urlparse(github_repo_url)
61
+ path_parts = parsed_url.path.strip('/').split('/')
62
+ if len(path_parts) == 2:
63
+ user_name, repo_name = path_parts
64
+ else:
65
+ st.error("Invalid GitHub repository URL. Please ensure it is in the format: https://github.com/user/repo")
66
+ st.stop()
67
+
68
+ REPO_URL = f"https://{github_token}@github.com/{user_name}/{repo_name}.git"
69
+
70
+ headers = {
71
+ 'Authorization': f'token {github_token}',
72
+ 'Accept': 'application/vnd.github.v3+json',
73
+ }
74
+
75
+ def force_clone_repo(*args, **kwargs) -> str:
76
+ if os.path.exists(repo_name):
77
+ shutil.rmtree(repo_name)
78
+ try:
79
+ Repo.clone_from(REPO_URL, repo_name)
80
+ return f"Repository {repo_name} forcefully cloned successfully."
81
+ except GitCommandError as e:
82
+ return f"Error cloning repository: {str(e)}"
83
+
84
+ force_clone_tool = Tool(
85
+ name="force_clone_repo",
86
+ func=force_clone_repo,
87
+ description="Forcefully clone the repository, removing any existing local copy."
88
+ )
89
+
90
+ class WriteFileInput(BaseModel):
91
+ file_path: str = Field(..., description="The path of the file to write to")
92
+ content: str = Field(..., description="The content to write to the file")
93
+
94
+ def write_file_content(file_path: str, content: str) -> str:
95
+ full_path = os.path.join(repo_name, file_path)
96
+ try:
97
+ with open(full_path, 'w') as file:
98
+ file.write(content)
99
+ return f"Successfully wrote to {full_path}"
100
+ except Exception as e:
101
+ return f"Error writing to file: {str(e)}"
102
+
103
+ file_write_tool = StructuredTool.from_function(
104
+ func=write_file_content,
105
+ name="write_file",
106
+ description="Write content to a specific file in the repository.",
107
+ args_schema=WriteFileInput
108
+ )
109
+
110
+ def read_file_content(file_path: str) -> str:
111
+ force_clone_repo() # Ensure we have the latest version before reading
112
+ full_path = os.path.join(repo_name, file_path)
113
+ try:
114
+ with open(full_path, 'r') as file:
115
+ content = file.read()
116
+ return f"File content:\n{content}"
117
+ except Exception as e:
118
+ return f"Error reading file: {str(e)}"
119
+
120
+ file_read_tool = Tool(
121
+ name="read_file",
122
+ func=read_file_content,
123
+ description="Read content from a specific file in the repository."
124
+ )
125
+
126
+ class CommitPushInput(BaseModel):
127
+ commit_message: str = Field(..., description="The commit message")
128
+
129
+ def commit_and_push(commit_message: str) -> str:
130
+ try:
131
+ repo = Repo(repo_name)
132
+ repo.git.add(A=True)
133
+ repo.index.commit(commit_message)
134
+ origin = repo.remote(name='origin')
135
+ push_info = origin.push()
136
+
137
+ if push_info:
138
+ if push_info[0].flags & push_info[0].ERROR:
139
+ return f"Error pushing changes: {push_info[0].summary}"
140
+ else:
141
+ return f"Changes committed and pushed successfully with message: {commit_message}"
142
+ else:
143
+ return "No changes to push"
144
+ except GitCommandError as e:
145
+ return f"GitCommandError: {str(e)}"
146
+ except Exception as e:
147
+ return f"Unexpected error: {str(e)}"
148
+
149
+ commit_push_tool = StructuredTool.from_function(
150
+ func=commit_and_push,
151
+ name="commit_and_push",
152
+ description="Commit and push changes to the repository with a specific commit message.",
153
+ args_schema=CommitPushInput
154
+ )
155
+
156
+ tools = [force_clone_tool, file_read_tool, file_write_tool, commit_push_tool, ShellTool()]
157
+
158
+ class AgentState(TypedDict):
159
+ messages: Annotated[List[BaseMessage], operator.add]
160
+
161
+ llm = ChatAnthropic(temperature=0, model_name="claude-3-haiku-20240307")
162
+
163
+ system_prompt_template = """You are an AI specialized in managing and analyzing a GitHub repository for a Next.js blog website.
164
+ Your task is to answer user queries about the repository or execute tasks for modifying it.
165
+
166
+ Before performing any operation, always use the force_clone_repo tool to ensure you have the latest version of the repository.
167
+
168
+ Here is all of the code from the repository as well as the file paths for context of how the repo is structured: {REPO_CONTENT}
169
+
170
+ Given this context, follow this prompt in completing the user's task:
171
+ For user questions, provide direct answers based on the current state of the repository.
172
+ For tasks given by the user, use the available tools and your knowledge of the repo to make necessary changes to the repository.
173
+
174
+ When making changes, remember to force clone the repository first, make the changes, and then commit and push the changes.
175
+ Available tools:
176
+ 1. shell_tool: Execute shell commands
177
+ 2. write_file: Write content to a specific file. Use as: write_file(file_path: str, content: str)
178
+ 3. force_clone_repo: Forcefully clone the repository, removing any existing local copy
179
+ 4. commit_and_push: Commit and push changes to the repository
180
+ 5. read_file: Read content from a specific file in the repository
181
+ When using the write_file tool, always provide both the file_path and the content as separate arguments.
182
+
183
+ Respond to the human's messages and use tools when necessary to complete tasks. Take a deep breath and think through the task step by step:"""
184
+
185
+ from langgraph.checkpoint import MemorySaver
186
+
187
+ memory = MemorySaver()
188
+
189
+ def extract_repo_info(url):
190
+ parts = url.split('/')
191
+ if 'github.com' not in parts:
192
+ raise ValueError("Not a valid GitHub URL")
193
+
194
+ owner = parts[parts.index('github.com') + 1]
195
+ repo = parts[parts.index('github.com') + 2]
196
+
197
+ path_start_index = parts.index(repo) + 1
198
+ if path_start_index < len(parts) and parts[path_start_index] == 'tree':
199
+ path_start_index += 2
200
+
201
+ path = '/'.join(parts[path_start_index:])
202
+
203
+ return owner, repo, path
204
+
205
+ def get_repo_contents(owner, repo, path=''):
206
+ api_url = f'https://api.github.com/repos/{owner}/{repo}/contents/{path}'
207
+ response = requests.get(api_url, headers=headers)
208
+ return response.json()
209
+
210
+ def get_file_content_and_metadata(file_url):
211
+ response = requests.get(file_url, headers=headers)
212
+ content_data = response.json()
213
+ content = content_data.get('content', '')
214
+
215
+ if content:
216
+ try:
217
+ decoded_content = base64.b64decode(content)
218
+ decoded_content_str = decoded_content.decode('utf-8')
219
+ except (base64.binascii.Error, UnicodeDecodeError):
220
+ decoded_content_str = content
221
+ else:
222
+ decoded_content_str = ''
223
+
224
+ last_modified = content_data.get('last_modified') or response.headers.get('Last-Modified', '')
225
+
226
+ return decoded_content_str, last_modified
227
+
228
+ def is_valid_extension(filename):
229
+ valid_extensions = ['.ipynb', '.py', '.js', '.md', '.mdx', 'tsx', 'ts', 'css', '.json']
230
+ return any(filename.endswith(ext) for ext in valid_extensions)
231
+
232
+ def process_repo(repo_url):
233
+ owner, repo, initial_path = extract_repo_info(repo_url)
234
+ result = []
235
+ stack = [(initial_path, f'https://api.github.com/repos/{owner}/{repo}/contents/{initial_path}')]
236
+
237
+ while stack:
238
+ path, url = stack.pop()
239
+ contents = get_repo_contents(owner, repo, path)
240
+
241
+ if isinstance(contents, dict) and 'message' in contents:
242
+ print(f"Error: {contents['message']}")
243
+ return []
244
+
245
+ for item in contents:
246
+ if item['type'] == 'file':
247
+ if is_valid_extension(item['name']):
248
+ file_url = item['url']
249
+ file_content, last_modified = get_file_content_and_metadata(file_url)
250
+ if file_content:
251
+ result.append({
252
+ 'url': item['html_url'],
253
+ 'markdown': file_content,
254
+ 'last_modified': last_modified
255
+ })
256
+ elif item['type'] == 'dir':
257
+ stack.append((item['path'], item['url']))
258
+
259
+ return result
260
+
261
+ def refresh_repo_data():
262
+ repo_contents = process_repo(github_repo_url)
263
+ repo_contents_json = json.dumps(repo_contents, ensure_ascii=False, indent=2)
264
+ st.session_state.REPO_CONTENT = repo_contents_json
265
+ st.success("Repository content refreshed successfully.")
266
+
267
+ # Update the system prompt with the new repo content
268
+ st.session_state.system_prompt = system_prompt_template.format(REPO_CONTENT=st.session_state.REPO_CONTENT)
269
+
270
+ # Recreate the graph with the updated system prompt
271
+ global graph
272
+ graph = create_react_agent(
273
+ llm,
274
+ tools=tools,
275
+ messages_modifier=st.session_state.system_prompt,
276
+ checkpointer=memory
277
+ )
278
+
279
+ # Automatically refresh repo data when keys are provided
280
+ if "REPO_CONTENT" not in st.session_state:
281
+ refresh_repo_data()
282
+
283
+ # Initialize system_prompt in session state
284
+ if "system_prompt" not in st.session_state:
285
+ st.session_state.system_prompt = system_prompt_template.format(REPO_CONTENT=st.session_state.REPO_CONTENT)
286
+
287
+ graph = create_react_agent(
288
+ llm,
289
+ tools=tools,
290
+ messages_modifier=st.session_state.system_prompt,
291
+ checkpointer=memory
292
+ )
293
+
294
+ from langchain_core.messages import AIMessage, ToolMessage
295
+
296
+ async def run_github_editor(query: str, thread_id: str = "default"):
297
+ inputs = {"messages": [HumanMessage(content=query)]}
298
+ config = {
299
+ "configurable": {"thread_id": thread_id},
300
+ "recursion_limit": 50 # Add this line to set the recursion limit
301
+ }
302
+
303
+ st.write(f"Human: {query}\n")
304
+
305
+ current_thought = ""
306
+
307
+ async for event in graph.astream_events(inputs, config=config, version="v2"):
308
+ kind = event["event"]
309
+ if kind == "on_chat_model_start":
310
+ st.write("AI is thinking...")
311
+ elif kind == "on_chat_model_stream":
312
+ data = event["data"]
313
+ if data["chunk"].content:
314
+ content = data["chunk"].content
315
+ if isinstance(content, list) and content and isinstance(content[0], dict):
316
+ text = content[0].get('text', '')
317
+ current_thought += text
318
+ if text.endswith(('.', '?', '!')):
319
+ st.write(current_thought.strip())
320
+ current_thought = ""
321
+ else:
322
+ st.write(content, end="")
323
+ elif kind == "on_tool_start":
324
+ st.write(f"\nUsing tool: {event['name']}")
325
+ elif kind == "on_tool_end":
326
+ st.write(f"Tool result: {event['data']['output']}\n")
327
+
328
+ # Create a session state variable to store the chat messages. This ensures that the
329
+ # messages persist across reruns.
330
+ if "messages" not in st.session_state:
331
+ st.session_state.messages = []
332
+
333
+ # Display the current system prompt if show_system_prompt is True
334
+ if st.session_state.show_system_prompt:
335
+ st.text_area("Current System Prompt", st.session_state.system_prompt, height=300)
336
+
337
+ # Display the existing chat messages via `st.chat_message`.
338
+ for message in st.session_state.messages:
339
+ with st.chat_message(message["role"]):
340
+ st.markdown(message["content"])
341
+
342
+ # Create a chat input field to allow the user to enter a message. This will display
343
+ # automatically at the bottom of the page.
344
+ if prompt := st.chat_input("What is up?"):
345
+
346
+ # Store and display the current prompt.
347
+ st.session_state.messages.append({"role": "user", "content": prompt})
348
+ with st.chat_message("user"):
349
+ st.markdown(prompt)
350
+
351
+ # Generate a response using the custom chatbot logic.
352
+ asyncio.run(run_github_editor(prompt))