Skip to content

Commit cb96b63

Browse files
authored
Merge pull request #2 from ississippi/convert-to-dynamodb
Convert to dynamodb, begin conversation capability
2 parents 7fd4e16 + 4ac232c commit cb96b63

File tree

9 files changed

+396
-41
lines changed

9 files changed

+396
-41
lines changed

pr_chat/chat_ws_api.py

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,90 @@
66
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
77
from fastapi.staticfiles import StaticFiles
88
from fastapi.responses import FileResponse
9+
from contextlib import asynccontextmanager
10+
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
911
from langchain_anthropic import ChatAnthropic
1012
from langchain_core.prompts import ChatPromptTemplate
1113
from langchain_core.output_parsers import StrOutputParser
1214
from langchain_core.runnables import RunnableWithMessageHistory
13-
from langchain.memory.chat_message_histories import RedisChatMessageHistory
15+
import traceback
16+
17+
class PatchedDynamoDBChatMessageHistory(DynamoDBChatMessageHistory):
18+
@property
19+
def key(self):
20+
return {"id": self.session_id}
21+
1422

1523
# FastAPI app
1624
app = FastAPI()
25+
llm = None
26+
ANTHROPIC_API_KEY = None
27+
@asynccontextmanager
28+
async def lifespan(app: FastAPI):
29+
global ANTHROPIC_API_KEY
30+
ssm = boto3.client('ssm', region_name='us-east-1')
31+
ANTHROPIC_API_KEY = ssm.get_parameter(
32+
Name="/prreview/ANTHROPIC_API_KEY",
33+
WithDecryption=True
34+
)['Parameter']['Value']
35+
os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY
36+
os.environ["AWS_REGION"] = "us-east-1"
37+
global llm
38+
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", temperature=0.7)
1739

18-
# Parameter Store (SSM) for API key
19-
ssm = boto3.client('ssm', region_name='us-east-1')
20-
ANTHROPIC_API_KEY = ssm.get_parameter(
21-
Name="/prreview/ANTHROPIC_API_KEY",
22-
WithDecryption=True
23-
)['Parameter']['Value']
24-
os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY
40+
yield # app is now running
2541

26-
# Claude setup
27-
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", temperature=0.7)
42+
app = FastAPI(lifespan=lifespan)
43+
session = boto3.Session(region_name="us-east-1")
2844

2945
# Prompt
3046
prompt = ChatPromptTemplate.from_messages([
3147
("system", "You're a helpful assistant."),
3248
("human", "{input}")
3349
])
34-
35-
# Redis config
36-
redis_host = os.environ.get("REDIS_HOST", "localhost")
37-
redis_port = int(os.environ.get("REDIS_PORT", "6379"))
38-
39-
# Chain with memory
50+
# DDB for chat history
4051
def get_history(session_id: str):
41-
return RedisChatMessageHistory(session_id=session_id, url=f"redis://{redis_host}:{redis_port}")
52+
print(f"[DEBUG] Using session_id={session_id} (type: {type(session_id)})")
53+
return DynamoDBChatMessageHistory(
54+
table_name="ChatMemory",
55+
session_id=session_id,
56+
key={"id": session_id}
57+
)
4258

43-
chat_chain = RunnableWithMessageHistory(
44-
prompt | llm | StrOutputParser(),
45-
get_session_history=get_history,
46-
input_messages_key="input",
47-
history_messages_key="messages"
48-
)
59+
def get_chat_chain():
60+
if llm is None:
61+
raise RuntimeError("LLM not initialized yet")
62+
return RunnableWithMessageHistory(
63+
prompt | llm | StrOutputParser(),
64+
get_session_history=get_history,
65+
input_messages_key="input",
66+
history_messages_key="messages"
67+
)
4968

5069
# WebSocket chat endpoint
5170
@app.websocket("/ws/chat/{user_id}/{session_id}")
5271
async def websocket_chat(websocket: WebSocket, user_id: str, session_id: str):
5372
await websocket.accept()
5473
try:
74+
if ANTHROPIC_API_KEY is None:
75+
print("🔑 ANTHROPIC_API_KEY not set. Cannot connect to Claude.")
76+
else:
77+
print(f"🔑 ANTHROPIC_API_KEY length is: {len(ANTHROPIC_API_KEY)}. Connected to Claude.")
5578
while True:
5679
message = await websocket.receive_text()
5780
print(f"Received message from {user_id}/{session_id}: {message}")
5881

59-
response = chat_chain.invoke(
60-
{"input": message},
61-
config={"configurable": {"session_id": session_id}}
62-
)
82+
try:
83+
response = get_chat_chain().invoke(
84+
{"input": message},
85+
config={"configurable": {"session_id": session_id}}
86+
)
87+
await websocket.send_text(response)
6388

64-
await websocket.send_text(response)
89+
except Exception as e:
90+
print("🔥 LLM invocation failed:")
91+
traceback.print_exc()
92+
await websocket.send_text(f"[Server Error] {str(e)}")
6593

6694
except WebSocketDisconnect:
6795
print(f"Client disconnected: {user_id}/{session_id}")

pr_chat/docker-compose.yml

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,10 @@ services:
33
build:
44
context: .
55
dockerfile: Dockerfile
6-
container_name: chat-api
6+
container_name: chat-ws-api
77
ports:
88
- "8080:8080"
99
environment:
10-
REDIS_HOST: redis
11-
REDIS_PORT: 6379
1210
AWS_REGION: us-east-1
1311
volumes:
1412
- ~/.aws:/root/.aws:ro
15-
depends_on:
16-
- redis
17-
18-
redis:
19-
image: redis:7
20-
container_name: chat-redis
21-
ports:
22-
- "6379:6379"
23-
restart: always

pr_chat/git_provider.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import os
2+
import boto3
3+
import json
4+
import time
5+
import requests
6+
from github import Github
7+
from github import Auth
8+
from datetime import datetime, timezone
9+
from dateutil import tz
10+
from zoneinfo import ZoneInfo
11+
from pathlib import Path
12+
13+
owner = "TheAlgorithms"
14+
repo = "Python"
15+
owner = "public-apis"
16+
repo = "public-apis"
17+
pull_number = 25653 # Replace with the desired PR number
18+
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}"
19+
pulls_url = f"https://api.github.com/repos/{owner}/{repo}/pulls"
20+
# vinta_pulls = "https://api.github.com/vinta/awesome-python/pulls" # Example for a different repo
21+
SUPPORTED_EXTENSIONS = {'.py', '.js', '.ts', '.java', '.cs', '.cpp', '.c', '.go', '.rb'}
22+
23+
# Create an SSM client
24+
ssm = boto3.client('ssm')
25+
def get_parameter(name):
26+
"""Fetch parameter value from Parameter Store"""
27+
response = ssm.get_parameter(
28+
Name=name,
29+
WithDecryption=True
30+
)
31+
return response['Parameter']['Value']
32+
33+
# Load secrets from AWS at cold start
34+
GIT_API_KEY = get_parameter("/prreview/GIT_API_KEY")
35+
if GIT_API_KEY is None:
36+
raise ValueError("GIT_API_KEY was not found in the parameter store.")
37+
38+
headers = {
39+
"Accept": "application/vnd.github.v3+json",
40+
# Optional: Add token for higher rate limits
41+
# "Authorization": "Bearer YOUR_TOKEN"
42+
}
43+
44+
def get_pr_details():
45+
response = requests.get(url, headers=headers)
46+
if response.status_code == 200:
47+
pr_data = response.json()
48+
print("PR Title:", pr_data["title"])
49+
print("Source Branch:", pr_data["head"]["ref"])
50+
print("Target Branch:", pr_data["base"]["ref"])
51+
print("Diff URL:", pr_data["diff_url"])
52+
else:
53+
print(f"Error: {response.status_code}, {response.json().get('message')}")
54+
55+
def get_pr_diff(repo,pr_number):
56+
# Headers for diff request
57+
diff_headers = {
58+
"Accept": "application/vnd.github.v3.diff",
59+
}
60+
token = GIT_API_KEY
61+
# print(f"Using GitHub API Key: {token}")
62+
if token:
63+
headers["Authorization"] = f"token {token}"
64+
65+
# Construct the diff URL
66+
url = f"https://github.com/{repo}/pull/{pr_number}.diff"
67+
# Get the diff for this PR
68+
# print(f"Fetching diff for PR #{pr_number} in {repo}...")
69+
response = requests.get(url, headers=diff_headers)
70+
if response.status_code == 200:
71+
diff = response.text
72+
#print(f'Diff: {diff}')
73+
return diff
74+
else:
75+
print(f"Error: {response.status_code}")
76+
77+
def get_supported_diffs(repo, pr_number):
78+
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files"
79+
headers = {
80+
"Authorization": f"token {GIT_API_KEY}",
81+
"Accept": "application/vnd.github.v3+json"
82+
}
83+
response = requests.get(url, headers=headers)
84+
response.raise_for_status()
85+
all_files = response.json()
86+
#print(f"File: {all_files[0]}")
87+
88+
89+
# Check if the response contains a list of files
90+
91+
# Keep only files with a supported extension
92+
supported_diffs = [
93+
file for file in all_files
94+
if os.path.splitext(file['filename'])[1] in SUPPORTED_EXTENSIONS and 'patch' in file
95+
]
96+
if len(all_files) != len(supported_diffs):
97+
print(f"Found {len(all_files) - len(supported_diffs)} out of {len(all_files)} filetypes in diffs which are not supported for PR #{pr_number} in {repo}.")
98+
99+
return supported_diffs
100+
101+
def get_pr_files(repo, pr_number):
102+
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files"
103+
# print(f"Using GitHub API Key: {token}")
104+
headers = {
105+
"Authorization": f"Bearer {GIT_API_KEY}",
106+
"Accept": "application/vnd.github.v3+json",
107+
}
108+
response = requests.get(url, headers=headers)
109+
response.raise_for_status() # Raise an error for bad responses
110+
111+
files = response.json()
112+
for file in files:
113+
filename = file.get("filename")
114+
status = file.get("status") # e.g. 'added', 'modified', 'removed'
115+
print(f"{status.upper()}: {filename}")
116+
117+
return files
118+
119+
120+
def get_pull_requests(state='open'):
121+
"""
122+
Fetch pull requests from a GitHub repository.
123+
124+
Args:
125+
owner (str): Repository owner (e.g., 'octocat')
126+
repo (str): Repository name (e.g., 'hello-world')
127+
token (str, optional): GitHub Personal Access Token for authentication
128+
state (str): State of PRs to fetch ('open', 'closed', or 'all')
129+
130+
Returns:
131+
list: List of pull requests
132+
"""
133+
params = {
134+
"state": state,
135+
"per_page": 5 # Maximum number of PRs per page
136+
}
137+
138+
pull_requests = []
139+
page = 1
140+
print(f"Fetching {state} pull requests from {pulls_url}...")
141+
#while True:
142+
params["page"] = page
143+
response = requests.get(pulls_url, headers=headers, params=params)
144+
145+
if response.status_code != 200:
146+
print(f"Error: {response.status_code} - {response.json().get('message', 'Unknown error')}")
147+
return
148+
149+
prs = response.json()
150+
# if not prs: # No more PRs to fetch
151+
# break
152+
153+
pull_requests.extend(prs)
154+
#page += 1
155+
156+
return pull_requests
157+
158+
def print_pull_requests(prs):
159+
"""
160+
Print basic information about pull requests.
161+
162+
Args:
163+
prs (list): List of pull requests
164+
"""
165+
for pr in prs:
166+
print(f"State: {pr['state'].capitalize()}")
167+
print(f"{pr['title']}")
168+
user_name = pr['user']['login'] if 'user' in pr else 'Unknown User'
169+
created_at = pr['created_at']
170+
if created_at:
171+
local_timezone = tz.tzlocal()
172+
date_object = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ")
173+
local_time = date_object.astimezone(local_timezone)
174+
175+
created_at = local_time.strftime("%Y-%m-%d at %H:%M:%S")
176+
else:
177+
created_at = "Unknown Date"
178+
print(f"PR #{pr['number']} opened by {user_name} on {created_at}")
179+
180+
print(f"URL: {pr['html_url']}")
181+
print("-" * 50)
182+
183+
def post_review(repo, pr_number, decision, review):
184+
headers = {
185+
"Accept": "application/vnd.github.v3.diff",
186+
"X-GitHub-Api-Version" : "2022-11-28"
187+
}
188+
if GIT_API_KEY:
189+
headers["Authorization"] = f"token {GIT_API_KEY}"
190+
191+
payload = {
192+
"body": f"{review}",
193+
"event": f"{decision}",
194+
"comments": [
195+
{
196+
"path": "path/to/file.py",
197+
"position": 1,
198+
"body": "Please change this line to improve readability."
199+
}
200+
]
201+
}
202+
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews"
203+
response = requests.post(url, headers=headers)
204+
if response.status_code == 200:
205+
review_data = response.json()
206+
print("Review submitted successfully.")
207+
print(f"Review ID: {review_data['id']}")
208+
print(f"State: {review_data['state']}")
209+
print(f"Submitted by: {review_data['user']['login']}")
210+
print(f"HTML URL: {review_data['html_url']}")
211+
else:
212+
print(f"Failed to submit review: {response.status_code} - {response.text}")
213+
214+
215+
def request_review(repo, pr_number, reviewer):
216+
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/requested_reviewers"
217+
headers = {
218+
"Authorization": f"token {GIT_API_KEY}",
219+
"Accept": "application/vnd.github+json"
220+
}
221+
payload = {
222+
"reviewers": [reviewer]
223+
# Optional: "team_reviewers": ["team-slug"]
224+
}
225+
226+
response = requests.post(url, headers=headers, json=payload)
227+
228+
if response.status_code == 201:
229+
print("Reviewers requested successfully.")
230+
else:
231+
print(f"Failed to request reviewers: {response.status_code} - {response.text}")
232+
233+
234+
if __name__ == "__main__":
235+
# repo = "ississippi/pull-request-test-repo"
236+
repo = "ississippi/pull-request-automated-review"
237+
pr_number = 13
238+
# get_pr_details()
239+
# get_pr_files()
240+
# get_pr_diff("ississippi/pull-request-test-repo", 16)
241+
# Fetch pull requests
242+
# prs = get_pull_requests()
243+
# # Print results
244+
# print(f"Found {len(prs)} pull requests:")
245+
# print_pull_requests(prs)
246+
# git_pr_list() : needs work
247+
# get_pr_files(owner=owner,repo=repo,pr_number=pr_number)
248+
# request_review(repo, 10, "ississippi")
249+
# decision = "REQUEST_CHANGES"
250+
# review = "This is close to perfect! Please address the suggested inline change."
251+
# post_review(repo, 10, decision, review)
252+
get_supported_diffs(repo, pr_number)

pr_chat/static/favicon.ico

15 KB
Binary file not shown.

0 commit comments

Comments
 (0)