sunholo 0.59.1__py3-none-any.whl → 0.59.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ # from https://github.com/ray-project/docu-mentor
2
+ import base64
3
+ import httpx
4
+ from dotenv import load_dotenv
5
+ import jwt
6
+ import os
7
+ import time
8
+
9
+ load_dotenv()
10
+
11
+
12
+
13
+ APP_ID = os.environ.get("APP_ID")
14
+ PRIVATE_KEY = os.environ.get("PRIVATE_KEY", "")
15
+
16
+ # with open('private-key.pem', 'r') as f:
17
+ # PRIVATE_KEY = f.read()
18
+
19
+ def generate_jwt():
20
+ payload = {
21
+ "iat": int(time.time()),
22
+ "exp": int(time.time()) + (10 * 60),
23
+ "iss": APP_ID,
24
+ }
25
+ if PRIVATE_KEY:
26
+ jwt_token = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
27
+ return jwt_token
28
+ raise ValueError("PRIVATE_KEY not found.")
29
+
30
+
31
+ async def get_installation_access_token(jwt, installation_id):
32
+ url = f"https://api.github.com/app/installations/{installation_id}/access_tokens"
33
+ headers = {
34
+ "Authorization": f"Bearer {jwt}",
35
+ "Accept": "application/vnd.github.v3+json",
36
+ }
37
+ async with httpx.AsyncClient() as client:
38
+ response = await client.post(url, headers=headers)
39
+ return response.json()["token"]
40
+
41
+
42
+ def get_diff_url(pr):
43
+ """GitHub 302s to this URL."""
44
+ original_url = pr.get("url")
45
+ parts = original_url.split("/")
46
+ owner, repo, pr_number = parts[-4], parts[-3], parts[-1]
47
+ return f"https://patch-diff.githubusercontent.com/raw/{owner}/{repo}/pull/{pr_number}.diff"
48
+
49
+
50
+ async def get_branch_files(pr, branch, headers):
51
+ original_url = pr.get("url")
52
+ parts = original_url.split("/")
53
+ owner, repo = parts[-4], parts[-3]
54
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
55
+ async with httpx.AsyncClient() as client:
56
+ response = await client.get(url, headers=headers)
57
+ tree = response.json().get('tree', [])
58
+ files = {}
59
+ for item in tree:
60
+ if item['type'] == 'blob':
61
+ file_url = item['url']
62
+ print(file_url)
63
+ file_response = await client.get(file_url, headers=headers)
64
+ content = file_response.json().get('content', '')
65
+ # Decode the base64 content
66
+ decoded_content = base64.b64decode(content).decode('utf-8')
67
+ files[item['path']] = decoded_content
68
+ return files
69
+
70
+
71
+ async def get_pr_head_branch(pr, headers):
72
+ original_url = pr.get("url")
73
+ parts = original_url.split("/")
74
+ owner, repo, pr_number = parts[-4], parts[-3], parts[-1]
75
+ url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
76
+
77
+ async with httpx.AsyncClient() as client:
78
+ response = await client.get(url, headers=headers)
79
+
80
+ # Check if the response is successful
81
+ if response.status_code != 200:
82
+ print(f"Error: Received status code {response.status_code}")
83
+ print("Response body:", response.text)
84
+ return ''
85
+
86
+ # Safely get the 'ref'
87
+ data = response.json()
88
+ head_data = data.get('head', {})
89
+ ref = head_data.get('ref', '')
90
+ return ref
91
+
92
+
93
+ def files_to_diff_dict(diff):
94
+ files_with_diff = {}
95
+ current_file = None
96
+ for line in diff.split("\n"):
97
+ if line.startswith("diff --git"):
98
+ current_file = line.split(" ")[2][2:]
99
+ files_with_diff[current_file] = {"text": []}
100
+ elif line.startswith("+") and not line.startswith("+++"):
101
+ files_with_diff[current_file]["text"].append(line[1:])
102
+ return files_with_diff
103
+
104
+
105
+ def parse_diff_to_line_numbers(diff):
106
+ files_with_line_numbers = {}
107
+ current_file = None
108
+ line_number = 0
109
+ for line in diff.split("\n"):
110
+ if line.startswith("diff --git"):
111
+ current_file = line.split(" ")[2][2:]
112
+ files_with_line_numbers[current_file] = []
113
+ line_number = 0
114
+ elif line.startswith("@@"):
115
+ line_number = int(line.split(" ")[2].split(",")[0][1:]) - 1
116
+ elif line.startswith("+") and not line.startswith("+++"):
117
+ files_with_line_numbers[current_file].append(line_number)
118
+ line_number += 1
119
+ elif not line.startswith("-"):
120
+ line_number += 1
121
+ return files_with_line_numbers
122
+
123
+
124
+ def get_context_from_files(files, files_with_line_numbers, context_lines=2):
125
+ context_data = {}
126
+ for file, lines in files_with_line_numbers.items():
127
+ file_content = files[file].split("\n")
128
+ context_data[file] = []
129
+ for line in lines:
130
+ start = max(line - context_lines, 0)
131
+ end = min(line + context_lines + 1, len(file_content))
132
+ context_data[file].append('\n'.join(file_content[start:end]))
133
+ return context_data
134
+
135
+ app = FastAPI()
136
+
137
+
138
+ async def handle_webhook(request: Request):
139
+ data = await request.json()
140
+
141
+ installation = data.get("installation")
142
+ if installation and installation.get("id"):
143
+ installation_id = installation.get("id")
144
+ logger.info(f"Installation ID: {installation_id}")
145
+
146
+ JWT_TOKEN = generate_jwt()
147
+
148
+ installation_access_token = await get_installation_access_token(
149
+ JWT_TOKEN, installation_id
150
+ )
151
+
152
+ headers = {
153
+ "Authorization": f"token {installation_access_token}",
154
+ "User-Agent": "docu-mentor-bot",
155
+ "Accept": "application/vnd.github.VERSION.diff",
156
+ }
157
+ else:
158
+ raise ValueError("No app installation found.")
159
+
160
+ # If PR exists and is opened
161
+ if "pull_request" in data.keys() and (
162
+ data["action"] in ["opened", "reopened"]
163
+ ): # use "synchronize" for tracking new commits
164
+ pr = data.get("pull_request")
165
+
166
+ # Greet the user and show instructions.
167
+ async with httpx.AsyncClient() as client:
168
+ await client.post(
169
+ f"{pr['issue_url']}/comments",
170
+ json={"body": GREETING},
171
+ headers=headers,
172
+ )
173
+ return JSONResponse(content={}, status_code=200)
174
+
175
+ # Check if the event is a new or modified issue comment
176
+ if "issue" in data.keys() and data.get("action") in ["created", "edited"]:
177
+ issue = data["issue"]
178
+
179
+ # Check if the issue is a pull request
180
+ if "/pull/" in issue["html_url"]:
181
+ pr = issue.get("pull_request")
182
+
183
+ # Get the comment body
184
+ comment = data.get("comment")
185
+ comment_body = comment.get("body")
186
+ # Remove all whitespace characters except for regular spaces
187
+ comment_body = comment_body.translate(
188
+ str.maketrans("", "", string.whitespace.replace(" ", ""))
189
+ )
190
+
191
+ # Skip if the bot talks about itself
192
+ author_handle = comment["user"]["login"]
193
+
194
+ # Check if the bot is mentioned in the comment
195
+ if (
196
+ author_handle != "docu-mentor[bot]"
197
+ and "@docu-mentor run" in comment_body
198
+ ):
199
+ async with httpx.AsyncClient() as client:
200
+ # Fetch diff from GitHub
201
+ files_to_keep = comment_body.replace(
202
+ "@docu-mentor run", ""
203
+ ).split(" ")
204
+ files_to_keep = [item for item in files_to_keep if item]
205
+
206
+ logger.info(files_to_keep)
207
+
208
+ url = get_diff_url(pr)
209
+ diff_response = await client.get(url, headers=headers)
210
+ diff = diff_response.text
211
+
212
+ files_with_lines = parse_diff_to_line_numbers(diff)
213
+
214
+ # Get head branch of the PR
215
+ headers["Accept"] = "application/vnd.github.full+json"
216
+ head_branch = await get_pr_head_branch(pr, headers)
217
+
218
+ # Get files from head branch
219
+ head_branch_files = await get_branch_files(pr, head_branch, headers)
220
+ print("HEAD FILES", head_branch_files)
221
+
222
+ # Enrich diff data with context from the head branch.
223
+ context_files = get_context_from_files(head_branch_files, files_with_lines)
224
+
225
+ # Filter the dictionary
226
+ if files_to_keep:
227
+ context_files = {
228
+ k: context_files[k]
229
+ for k in context_files
230
+ if any(sub in k for sub in files_to_keep)
231
+ }
232
+
233
+ # Get suggestions from Docu Mentor
234
+ content, model, prompt_tokens, completion_tokens = \
235
+ ray_mentor(context_files) if ray.is_initialized() else mentor(context_files)
236
+
237
+
238
+ # Let's comment on the PR
239
+ await client.post(
240
+ f"{comment['issue_url']}/comments",
241
+ json={
242
+ "body": f":rocket: Docu Mentor finished "
243
+ + "analysing your PR! :rocket:\n\n"
244
+ + "Take a look at your results:\n"
245
+ + f"{content}\n\n"
246
+ + "This bot is powered by "
247
+ + "[Sunholo Multivac](https://www.sunholo.com/).\n"
248
+ + f"It used the model {model}, used {prompt_tokens} prompt tokens, "
249
+ + f"and {completion_tokens} completion tokens in total."
250
+ },
251
+ headers=headers,
252
+ )
253
+
254
+ @serve.deployment(route_prefix="/")
255
+ @serve.ingress(app)
256
+ class ServeBot:
257
+ @app.get("/")
258
+ async def root(self):
259
+ return {"message": "Docu Mentor reporting for duty!"}
260
+
261
+ @app.post("/webhook/")
262
+ async def handle_webhook_route(self, request: Request):
263
+ return await handle_webhook(request)
264
+
sunholo/cli/configs.py CHANGED
@@ -12,8 +12,8 @@ def validate_config(config, schema):
12
12
  return True
13
13
  except ValidationError as err:
14
14
  error_path = " -> ".join(map(str, err.path))
15
- #print(f"ERROR: Validation error at '{error_path}': {err.message}")
16
- raise ValidationError(f"Validation error at '{error_path}': {err.message}")
15
+ print(f"ERROR: Validation error at '{error_path}': {err.message}")
16
+ return False
17
17
 
18
18
  def list_configs(args):
19
19
  """
@@ -84,16 +84,12 @@ def list_configs(args):
84
84
  print(f"Validating configuration for kind: {kind}")
85
85
  if args.kind == "vacConfig" and args.vac:
86
86
  print(f"Validating vacConfig for {args.vac}")
87
- try:
88
- validate_config(config[args.vac], VAC_SUBCONFIG_SCHEMA)
89
- except ValidationError as e:
90
- print(f"Validation failed for sub-kind: {args.vac} - {str(e)}")
87
+ if not validate_config(config[args.vac], VAC_SUBCONFIG_SCHEMA):
88
+ print(f"Validation failed for sub-kind: {args.vac}")
91
89
  validation_failed = True
92
90
  elif kind in SCHEMAS:
93
- try:
94
- validate_config(config, SCHEMAS[kind])
95
- except ValidationError as e:
96
- print(f"FAIL: Validation failed for kind: {kind} - - {str(e)}")
91
+ if not validate_config(config, SCHEMAS[kind]):
92
+ print(f"FAIL: Validation failed for kind: {kind}")
97
93
  validation_failed = True
98
94
  else:
99
95
  print(f"No schema available to validate configuration for kind: {kind}")
@@ -52,7 +52,8 @@ VAC_SUBCONFIG_SCHEMA = {
52
52
  "cluster": {"type": "string"},
53
53
  "instance": {"type": "string"},
54
54
  "database": {"type": "string"}
55
- }
55
+ },
56
+ "required": ["project_id", "region", "cluster", "instance", "database"]
56
57
  },
57
58
  "secrets": {
58
59
  "type": "array",
@@ -87,7 +88,7 @@ VAC_CONFIG_SCHEMA = {
87
88
  }
88
89
  }
89
90
  },
90
- "required": ["kind", "apiVersion", "gcp_config", "vac"]
91
+ "required": ["kind", "apiVersion", "vac"]
91
92
  }
92
93
 
93
94
  PROMPT_CONFIG_SCHEMA = {
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.59.1
3
+ Version: 0.59.3
4
4
  Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
5
5
  Home-page: https://github.com/sunholo-data/sunholo-py
6
- Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.59.1.tar.gz
6
+ Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.59.3.tar.gz
7
7
  Author: Holosun ApS
8
8
  Author-email: multivac@sunholo.com
9
9
  License: Apache License, Version 2.0
@@ -20,6 +20,7 @@ sunholo/auth/__init__.py,sha256=4owDjSaWYkbTlPK47UHTOC0gCWbZsqn4ZIEw5NWZTlg,28
20
20
  sunholo/auth/run.py,sha256=4fhNhDgtMtBHc08AywDmtazQPQ2560at6EMLQbMwTIo,2846
21
21
  sunholo/bots/__init__.py,sha256=EMFd7e2z68l6pzYOnkzHbLd2xJRvxTKFRNCTuhZ8hIw,130
22
22
  sunholo/bots/discord.py,sha256=cCFae5K1BCa6JVkWGLh_iZ9qFO1JpXb6K4eJrlDfEro,2442
23
+ sunholo/bots/github_webhook.py,sha256=5pQPRLM_wxxcILVaIzUDV8Kt7Arcm2dL1r1kMMHA524,9629
23
24
  sunholo/bots/webapp.py,sha256=EIMxdAJ_xtufwJmvnn7N_Fb_1hZ9DjhJ0Kf_hp02vEU,1926
24
25
  sunholo/chunker/__init__.py,sha256=UhQBZTKwDfBXm0TPv4LvsGc5pdUGCbYzi3qPTOkU4gw,55
25
26
  sunholo/chunker/data_to_embed_pubsub.py,sha256=t-pWNYv2mnwVAkMcIOK2CrIb3yr2aS9iAdtryk7hT8o,2931
@@ -33,7 +34,7 @@ sunholo/chunker/splitter.py,sha256=ug_v-h0wos3b7OkhmedVQs5jtLuDdFDWypvsZVYgxbU,6
33
34
  sunholo/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
35
  sunholo/cli/cli.py,sha256=rcO1hMthy5nWC_5sOHqRm7ut70c9JfxFTSjFRBNYuYg,1248
35
36
  sunholo/cli/cli_init.py,sha256=WReZuMQwDfkRUvssYT7TirUoG6SiT1dTDol8nLI8O70,3418
36
- sunholo/cli/configs.py,sha256=8oiFwFHTHzXtEK4AW3CLVcfcwt6rV9UTpM0v0_RW2I0,4820
37
+ sunholo/cli/configs.py,sha256=jHCNz_rANlQI2ZCWnlgJu5QwQc-a_Koi9Hm3XHjHEpE,4608
37
38
  sunholo/cli/deploy.py,sha256=zxdwUsRTRMC8U5vyRv0JiKBLFn84Ug_Tc88-_h9hJSs,1609
38
39
  sunholo/components/__init__.py,sha256=RJGNEihwvRIiDScKis04RHJv4yZGI1UpXlOmuCptNZI,208
39
40
  sunholo/components/llm.py,sha256=T4we3tGmqUj4tPwxQr9M6AXv_BALqZV_dRSvINan-oU,10374
@@ -85,14 +86,14 @@ sunholo/summarise/summarise.py,sha256=C3HhjepTjUhUC8FLk4jMQIBvq1BcORniwuTFHjPVhV
85
86
  sunholo/utils/__init__.py,sha256=G11nN_6ATjxpuMfG_BvcUr9UU8onPIgkpTK6CjOcbr8,48
86
87
  sunholo/utils/big_context.py,sha256=qHYtds4Ecf9eZRHVqXho4_q8Je7HD44-vS6RJ6s9Z0Q,5387
87
88
  sunholo/utils/config.py,sha256=Ve1sb68Av9_SPGqXs33g5FAJSIQ3GODoeuUCW3MNCwU,8802
88
- sunholo/utils/config_schema.py,sha256=bx3SHHuZ3SCOOXNRU91Mk-b4pHXt7D-EElv7Q85gAdw,3758
89
+ sunholo/utils/config_schema.py,sha256=Rkw5nVHcCtIQH_sH5ZUiDaxW1TOjUmzsDwHhrfiWeqQ,3829
89
90
  sunholo/utils/gcp.py,sha256=B2G1YKjeD7X9dqO86Jrp2vPuFwZ223Xl5Tg09Ndw-oc,5760
90
91
  sunholo/utils/parsers.py,sha256=OrHmASqIbI45atVOhiGodgLvnfrzkvVzyHnSvAXD89I,3841
91
92
  sunholo/vertex/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
93
  sunholo/vertex/init_vertex.py,sha256=JDMUaBRdednzbKF-5p33qqLit2LMsvgvWW-NRz0AqO0,1801
93
- sunholo-0.59.1.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
94
- sunholo-0.59.1.dist-info/METADATA,sha256=84L9QkQTPWbkcIAKuO6W__im6N66Da8UJ0400bYA_xA,7903
95
- sunholo-0.59.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
96
- sunholo-0.59.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
97
- sunholo-0.59.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
98
- sunholo-0.59.1.dist-info/RECORD,,
94
+ sunholo-0.59.3.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
95
+ sunholo-0.59.3.dist-info/METADATA,sha256=XAF0rooghlKcIPwDYbNNHt6dv5k06WF_PyV1t6GKl9E,7903
96
+ sunholo-0.59.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
97
+ sunholo-0.59.3.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
98
+ sunholo-0.59.3.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
99
+ sunholo-0.59.3.dist-info/RECORD,,