llama-github 0.1.0__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.
- llama_github/__init__.py +3 -0
- llama_github/config/__init__.py +0 -0
- llama_github/config/config.json +20 -0
- llama_github/config/config.py +30 -0
- llama_github/data_retrieval/__init__.py +0 -0
- llama_github/data_retrieval/github_api.py +296 -0
- llama_github/data_retrieval/github_entities.py +301 -0
- llama_github/features/__init__.py +0 -0
- llama_github/features/feature_flags.py +0 -0
- llama_github/features/insider_features.py +0 -0
- llama_github/github_integration/__init__.py +7 -0
- llama_github/github_integration/github_auth_manager.py +266 -0
- llama_github/github_rag.py +348 -0
- llama_github/llm_integration/__init__.py +0 -0
- llama_github/llm_integration/initial_load.py +101 -0
- llama_github/llm_integration/llm_handler.py +131 -0
- llama_github/logger.py +28 -0
- llama_github/rag_processing/__init__.py +0 -0
- llama_github/rag_processing/rag_processor.py +490 -0
- llama_github/utils.py +98 -0
- llama_github/version.py +1 -0
- llama_github-0.1.0.dist-info/LICENSE +201 -0
- llama_github-0.1.0.dist-info/METADATA +100 -0
- llama_github-0.1.0.dist-info/RECORD +26 -0
- llama_github-0.1.0.dist-info/WHEEL +5 -0
- llama_github-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# rag_processor.py
|
|
2
|
+
from llama_github.config.config import config
|
|
3
|
+
from llama_github.data_retrieval.github_api import GitHubAPIHandler
|
|
4
|
+
from llama_github.data_retrieval.github_entities import Repository
|
|
5
|
+
from llama_github.llm_integration.llm_handler import LLMManager, LLMHandler
|
|
6
|
+
from llama_github.logger import logger
|
|
7
|
+
from langchain_core.pydantic_v1 import BaseModel, Field
|
|
8
|
+
from typing import List, Optional, Dict
|
|
9
|
+
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from numpy.linalg import norm
|
|
13
|
+
import numpy as np
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RAGProcessor:
|
|
18
|
+
def __init__(self, github_api_handler: GitHubAPIHandler, llm_manager: LLMManager = None, llm_handler: LLMHandler = None):
|
|
19
|
+
if llm_manager:
|
|
20
|
+
self.llm_manager = llm_manager
|
|
21
|
+
else:
|
|
22
|
+
self.llm_manager = LLMManager()
|
|
23
|
+
|
|
24
|
+
if llm_handler:
|
|
25
|
+
self.llm_handler = llm_handler
|
|
26
|
+
else:
|
|
27
|
+
self.llm_handler = LLMHandler(llm_manager=self.llm_manager)
|
|
28
|
+
|
|
29
|
+
self.github_api_handler = github_api_handler
|
|
30
|
+
|
|
31
|
+
class _LLMFirstGenenralAnswer(BaseModel):
|
|
32
|
+
question: str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description="The abstraction of user's question, only one sentence no more than 20 words.",
|
|
35
|
+
example="How to create a NumPy array in Python?"
|
|
36
|
+
)
|
|
37
|
+
answer: str = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="The answer to the user's question, better with sample code.",
|
|
40
|
+
example="You can use the `numpy.array` function to create a NumPy array in Python. The sample code is as follows:\n\n```python\nimport numpy as np\n\narray = np.array([1, 2, 3])\nprint(array)\n```"
|
|
41
|
+
)
|
|
42
|
+
code_search_logic: str = Field(
|
|
43
|
+
...,
|
|
44
|
+
description="Simple logic analyze on how to search for Github code related to the user's question without detail search criteria nor keywords.",
|
|
45
|
+
)
|
|
46
|
+
issue_search_logic: str = Field(
|
|
47
|
+
...,
|
|
48
|
+
description="Simple logic analyze on how to search for Github issues related to the user's question without detail search criteria nor keywords.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def analyze_question(self, query: str) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
analyze user's question and generate strategy for code search and issue search
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
query (str): user's initial question.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: the answer of question.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
logger.debug(
|
|
63
|
+
f"Analyzing question and generating strategy")
|
|
64
|
+
prompt = config.get("always_answer_prompt")
|
|
65
|
+
response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, output_structure=self._LLMFirstGenenralAnswer)
|
|
66
|
+
return [response.question, response.answer, response.code_search_logic, response.issue_search_logic]
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Error in analyzing question: {e}")
|
|
69
|
+
return [response.question, ""]
|
|
70
|
+
|
|
71
|
+
class _GitHubCodeSearchCriteria(BaseModel):
|
|
72
|
+
search_criteria: List[str] = Field(
|
|
73
|
+
...,
|
|
74
|
+
description="A list of search criteria strings for GitHub code search, each following GitHub's search syntax.",
|
|
75
|
+
example=["NumPy Array language:python",
|
|
76
|
+
"log4j LoggingUtil language:java"],
|
|
77
|
+
min_items=1,
|
|
78
|
+
max_items=2
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def get_code_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
|
|
82
|
+
"""
|
|
83
|
+
generate Github search criteria based on user's question
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
query (str): user's initial question.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str[]: the search criteria for Github code search.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
logger.debug(
|
|
93
|
+
f"Generating code search criteria for question: {query}")
|
|
94
|
+
prompt = config.get("code_search_criteria_prompt")
|
|
95
|
+
response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubCodeSearchCriteria)
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"For {query}, the search_criterias for code search is: {response.search_criteria}")
|
|
98
|
+
return response.search_criteria
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Error in get_code_search_criteria: {e}")
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
class _GitHubRepoSearchCriteria(BaseModel):
|
|
104
|
+
necessity_score: int = Field(
|
|
105
|
+
...,
|
|
106
|
+
description="In case there is already Github Code search and issue search for question related context retrieve. How necessity do you think seperate repo search in Github will be required. 0-59:no necessity, 60-79:medium necessity, 80-100:high necessity",
|
|
107
|
+
example=65
|
|
108
|
+
)
|
|
109
|
+
search_criteria: List[str] = Field(
|
|
110
|
+
...,
|
|
111
|
+
description="A list of search criteria strings for GitHub repository search, each following GitHub's search syntax. The sorting of the list should be based on the necessity of the search criteria.",
|
|
112
|
+
example=["NumPy Array language:python",
|
|
113
|
+
"spring-boot log4j language:java"],
|
|
114
|
+
min_items=0,
|
|
115
|
+
max_items=2
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def get_repo_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
|
|
119
|
+
"""
|
|
120
|
+
generate Github search criteria based on user's question
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
query (str): user's initial question.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str[]: the search criteria for Github code search.
|
|
127
|
+
"""
|
|
128
|
+
search_criteria = []
|
|
129
|
+
try:
|
|
130
|
+
logger.debug(
|
|
131
|
+
f"Generating repo search criteria for question: {query}")
|
|
132
|
+
prompt = config.get("repo_search_criteria_prompt")
|
|
133
|
+
response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubRepoSearchCriteria)
|
|
134
|
+
if response.necessity_score >= 80:
|
|
135
|
+
search_criteria = response.search_criteria
|
|
136
|
+
elif response.necessity_score >= 60 and len(response.search_criteria) >= 1:
|
|
137
|
+
search_criteria = response.search_criteria[:1]
|
|
138
|
+
logger.debug(
|
|
139
|
+
f"For {query}, the search_criterias for repo search is: {search_criteria} and repo search necessity score is {response.necessity_score}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Error in get_repo_search_criteria: {e}")
|
|
142
|
+
return search_criteria
|
|
143
|
+
return search_criteria
|
|
144
|
+
|
|
145
|
+
def get_repo_simple_structure(self, repo: Repository) -> str:
|
|
146
|
+
"""
|
|
147
|
+
get a simple structure of a repository, only contains first 3 levels of repo folder/file structure.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
repo (Repository): the repository object.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
json: the simple structure of the repository.
|
|
154
|
+
"""
|
|
155
|
+
full_structure = repo.get_structure()
|
|
156
|
+
|
|
157
|
+
if not full_structure:
|
|
158
|
+
return json.dumps({})
|
|
159
|
+
|
|
160
|
+
def simplify_tree(tree, level=1):
|
|
161
|
+
"""
|
|
162
|
+
Simplify the tree structure to keep only three levels deep.
|
|
163
|
+
"""
|
|
164
|
+
if level > 3:
|
|
165
|
+
return '...'
|
|
166
|
+
|
|
167
|
+
simplified_tree = {}
|
|
168
|
+
for key, value in tree.items():
|
|
169
|
+
if 'children' in value:
|
|
170
|
+
simplified_tree[key] = {
|
|
171
|
+
'children': simplify_tree(value['children'], level + 1)
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
simplified_tree[key] = value
|
|
175
|
+
return simplified_tree
|
|
176
|
+
|
|
177
|
+
simplified_structure = simplify_tree(full_structure)
|
|
178
|
+
return json.dumps(simplified_structure, indent=4)
|
|
179
|
+
|
|
180
|
+
class _GitHubIssueSearchCriteria(BaseModel):
|
|
181
|
+
search_criteria: List[str] = Field(
|
|
182
|
+
...,
|
|
183
|
+
description="A list of search criteria strings for GitHub issue search, each following GitHub's search syntax.",
|
|
184
|
+
example=["is:open label:bug Numpy Array",
|
|
185
|
+
"is:closed label:documentation langchain ollama"],
|
|
186
|
+
min_items=1,
|
|
187
|
+
max_items=2
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def get_issue_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
|
|
191
|
+
"""
|
|
192
|
+
generate Github search criteria based on user's question
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
query (str): user's initial question.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str[]: the search criteria for Github issue search.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
logger.debug(
|
|
202
|
+
f"Generating issue search criteria for question: {query}")
|
|
203
|
+
prompt = config.get("issue_search_criteria_prompt")
|
|
204
|
+
response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubIssueSearchCriteria)
|
|
205
|
+
logger.debug(
|
|
206
|
+
f"For {query}, the search_criterias for issue search is: {response.search_criteria}")
|
|
207
|
+
return response.search_criteria
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(f"Error in get_issue_search_criteria: {e}")
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
def _arrange_code_search_result(self, code_search_result: List[Dict]) -> List[Dict]:
|
|
213
|
+
"""
|
|
214
|
+
Arrange the result of code search into chunks suitable for LLM RAG contexts.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
code_search_result (list): the result of code search.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
list: the arranged result of code search.
|
|
221
|
+
"""
|
|
222
|
+
arranged_results = []
|
|
223
|
+
|
|
224
|
+
for result in code_search_result:
|
|
225
|
+
content = result['content']
|
|
226
|
+
|
|
227
|
+
# Split content into chunks
|
|
228
|
+
chunks = self._split_content_into_chunks(
|
|
229
|
+
content, language=result['language'] if 'language' in result else None)
|
|
230
|
+
|
|
231
|
+
for chunk in chunks:
|
|
232
|
+
repository_full_name = result.get(
|
|
233
|
+
'repository_full_name', 'None')
|
|
234
|
+
description = result.get('description', 'None')
|
|
235
|
+
stargazers_count = result.get('stargazers_count', 'None')
|
|
236
|
+
updated_at = result.get('updated_at', 'None')
|
|
237
|
+
path = result.get('path', 'None')
|
|
238
|
+
language = result.get('language', 'None')
|
|
239
|
+
|
|
240
|
+
if updated_at != 'None':
|
|
241
|
+
updated_at = updated_at.strftime('%Y-%m-%d %H:%M:%S %Z')
|
|
242
|
+
|
|
243
|
+
chunk_text = (
|
|
244
|
+
f"Sample code from repository: {repository_full_name}\n"
|
|
245
|
+
f"repository description: {description}\n"
|
|
246
|
+
f"repository stars: {stargazers_count}\n"
|
|
247
|
+
f"repository last updated: {updated_at}\n"
|
|
248
|
+
f"code path in repository: {path}\n"
|
|
249
|
+
f"programming language is: {language}\n\n"
|
|
250
|
+
f"{chunk}"
|
|
251
|
+
)
|
|
252
|
+
arranged_results.append(chunk_text)
|
|
253
|
+
|
|
254
|
+
return arranged_results
|
|
255
|
+
|
|
256
|
+
def _split_content_into_chunks(self, content: str, language: Optional[str] = None, max_tokens: Optional[int] = config.get('chunk_size')) -> List[str]:
|
|
257
|
+
"""
|
|
258
|
+
Split the content into chunks of maximum token length using LangChain's RecursiveCharacterTextSplitter.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
content (str): The content to be split.
|
|
262
|
+
language (Optional[str]): The programming language of the code. Defaults to None.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
list: A list of content chunks.
|
|
266
|
+
"""
|
|
267
|
+
chunk_overlap = math.ceil(max_tokens * 0.15 / 100) * 100
|
|
268
|
+
|
|
269
|
+
if language is None or language.lower() not in [e.value for e in Language] or language.lower() in ['markdown', 'html', 'c', 'perl']:
|
|
270
|
+
splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
|
|
271
|
+
separators=[
|
|
272
|
+
"\n\n",
|
|
273
|
+
"\n",
|
|
274
|
+
"\r\n",
|
|
275
|
+
],
|
|
276
|
+
chunk_size=max_tokens,
|
|
277
|
+
chunk_overlap=chunk_overlap,
|
|
278
|
+
tokenizer=self.llm_manager.tokenizer
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
max_tokens = max_tokens * 5
|
|
282
|
+
chunk_overlap = chunk_overlap * 5
|
|
283
|
+
language_enum = Language[language.upper()]
|
|
284
|
+
splitter = RecursiveCharacterTextSplitter.from_language(
|
|
285
|
+
language=language_enum,
|
|
286
|
+
chunk_size=max_tokens,
|
|
287
|
+
chunk_overlap=chunk_overlap,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
chunks = splitter.split_text(content)
|
|
291
|
+
return chunks
|
|
292
|
+
|
|
293
|
+
def _arrange_issue_search_result(self, issue_search_result: dict) -> dict:
|
|
294
|
+
"""
|
|
295
|
+
arrange the result of issue search
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
issue_search_result (dict): the result of issue search.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
dict: the arranged result of issue search.
|
|
302
|
+
"""
|
|
303
|
+
arranged_results = []
|
|
304
|
+
|
|
305
|
+
for result in issue_search_result:
|
|
306
|
+
content = result['issue_content']
|
|
307
|
+
|
|
308
|
+
# Split content into chunks
|
|
309
|
+
chunks = self._split_content_into_chunks(
|
|
310
|
+
content, max_tokens=config.get('issue_chunk_size'))
|
|
311
|
+
|
|
312
|
+
for chunk in chunks:
|
|
313
|
+
arranged_results.append(chunk)
|
|
314
|
+
|
|
315
|
+
return arranged_results
|
|
316
|
+
|
|
317
|
+
def _arrange_repo_search_result(self, repo_search_result: dict) -> dict:
|
|
318
|
+
"""
|
|
319
|
+
arrange the result of repo search
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
repo_search_result (dict): the result of repo search.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
dict: the arranged result of repo search.
|
|
326
|
+
"""
|
|
327
|
+
arranged_results = []
|
|
328
|
+
|
|
329
|
+
for result in repo_search_result:
|
|
330
|
+
content = result['content']
|
|
331
|
+
|
|
332
|
+
# Split content into chunks
|
|
333
|
+
chunks = self._split_content_into_chunks(
|
|
334
|
+
content, max_tokens=config.get('repo_chunk_size'))
|
|
335
|
+
|
|
336
|
+
for chunk in chunks:
|
|
337
|
+
arranged_results.append(chunk)
|
|
338
|
+
|
|
339
|
+
return arranged_results
|
|
340
|
+
|
|
341
|
+
def _arrange_google_search_result(self, google_search_result: dict) -> dict:
|
|
342
|
+
"""
|
|
343
|
+
arrange the result of google search
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
google_search_result (dict): the result of google search.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
dict: the arranged result of google search.
|
|
350
|
+
"""
|
|
351
|
+
arranged_results = []
|
|
352
|
+
|
|
353
|
+
for result in google_search_result:
|
|
354
|
+
content = result['content']
|
|
355
|
+
|
|
356
|
+
# Split content into chunks
|
|
357
|
+
chunks = self._split_content_into_chunks(
|
|
358
|
+
content, max_tokens=config.get('google_chunk_size'))
|
|
359
|
+
|
|
360
|
+
for chunk in chunks:
|
|
361
|
+
arranged_results.append(chunk)
|
|
362
|
+
|
|
363
|
+
return arranged_results
|
|
364
|
+
|
|
365
|
+
def arrange_context(self, code_search_result: Optional[dict] = None, issue_search_result: Optional[dict] = None, repo_search_result: Optional[dict] = None, google_search_result: Optional[dict] = None) -> dict:
|
|
366
|
+
"""
|
|
367
|
+
arrange the context before RAG
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
code_search_result (dict, optional): the result of code search. Defaults to None.
|
|
371
|
+
issue_search_result (dict, optional): the result of issue search. Defaults to None.
|
|
372
|
+
repo_search_result (dict, optional): the result of repo search. Defaults to None.
|
|
373
|
+
google_search_result (dict, optional): the result of google search. Defaults to None.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
dict: the arranged context.
|
|
377
|
+
"""
|
|
378
|
+
context = []
|
|
379
|
+
if code_search_result:
|
|
380
|
+
context.extend(
|
|
381
|
+
self._arrange_code_search_result(code_search_result))
|
|
382
|
+
if issue_search_result:
|
|
383
|
+
context.extend(
|
|
384
|
+
self._arrange_issue_search_result(issue_search_result))
|
|
385
|
+
if repo_search_result:
|
|
386
|
+
context.extend(
|
|
387
|
+
self._arrange_repo_search_result(repo_search_result))
|
|
388
|
+
if google_search_result:
|
|
389
|
+
context.extend(
|
|
390
|
+
self._arrange_google_search_result(google_search_result))
|
|
391
|
+
return context
|
|
392
|
+
|
|
393
|
+
async def retrieve_topn_contexts(self, context_list: List[str], query: str, answer: Optional[str] = None, top_n: Optional[int] = 5) -> List[str]:
|
|
394
|
+
"""
|
|
395
|
+
Retrieve top n context strings from the context list.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
context_list (List[str]): List of context strings to retrieve top n from.
|
|
399
|
+
query (str): The query string.
|
|
400
|
+
answer (Optional[str]): The answer string (optional).
|
|
401
|
+
top_n (Optional[int]): Number of top context strings to retrieve (default: 5).
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List[str]: A list of top n context strings.
|
|
405
|
+
"""
|
|
406
|
+
top_contexts = []
|
|
407
|
+
try:
|
|
408
|
+
reranker = self.llm_manager.get_rerank_model()
|
|
409
|
+
|
|
410
|
+
sentence_pairs = [[query, doc] for doc in context_list]
|
|
411
|
+
rerank_scores = reranker.compute_score(sentence_pairs)
|
|
412
|
+
|
|
413
|
+
# Zip scores with contexts
|
|
414
|
+
scored_contexts = list(zip(rerank_scores, context_list))
|
|
415
|
+
sorted_scored_contexts = sorted(
|
|
416
|
+
scored_contexts, key=lambda x: x[0], reverse=True)
|
|
417
|
+
|
|
418
|
+
# Extract top 3*top_n contexts after rerank
|
|
419
|
+
selected_context = [context for score, context in sorted_scored_contexts[:min(
|
|
420
|
+
top_n*3, len(sorted_scored_contexts))]]
|
|
421
|
+
|
|
422
|
+
# If there are too few contexts, skip embedding comparison step
|
|
423
|
+
if len(selected_context) < top_n*2:
|
|
424
|
+
return selected_context[:min(top_n, len(selected_context))]
|
|
425
|
+
|
|
426
|
+
# Calculate embedding to select top 2*top_n
|
|
427
|
+
logger.debug("Embedding start...")
|
|
428
|
+
embedding_model = self.llm_manager.get_embedding_model()
|
|
429
|
+
query_embedding = embedding_model.encode(
|
|
430
|
+
query + "\n" + answer if answer is not None else "")
|
|
431
|
+
context_embeddings = [embedding_model.encode(
|
|
432
|
+
context) for context in selected_context]
|
|
433
|
+
|
|
434
|
+
cos_similarities = [(query_embedding @ context_embedding.T) / (norm(query_embedding) * norm(context_embedding))
|
|
435
|
+
for context_embedding in context_embeddings]
|
|
436
|
+
|
|
437
|
+
top_indices = np.argsort(cos_similarities)[-(top_n*2):][::-1]
|
|
438
|
+
top_contexts = [selected_context[i] for i in top_indices]
|
|
439
|
+
top_cos_similarities = [cos_similarities[i] for i in top_indices]
|
|
440
|
+
top_rerank_scores = [rerank_scores[context_list.index(
|
|
441
|
+
context)] for context in top_contexts]
|
|
442
|
+
|
|
443
|
+
# Use simple LLM to calculate context score
|
|
444
|
+
llm_scores = await asyncio.gather(*[self.get_context_relevance_score(query, context) for context in top_contexts])
|
|
445
|
+
logger.debug(f"Simple LLM scores: {llm_scores}")
|
|
446
|
+
|
|
447
|
+
# Combine scores for final ranking
|
|
448
|
+
combined_scores = [llm_score * cos_sim * rerank_score
|
|
449
|
+
for llm_score, cos_sim, rerank_score in zip(llm_scores, top_cos_similarities, top_rerank_scores)]
|
|
450
|
+
|
|
451
|
+
combined_context_scores = list(zip(top_contexts, combined_scores))
|
|
452
|
+
sorted_combined_context_scores = sorted(
|
|
453
|
+
combined_context_scores, key=lambda x: x[1], reverse=True)
|
|
454
|
+
logger.debug(
|
|
455
|
+
f"Combined sorted context scores: {sorted_combined_context_scores}")
|
|
456
|
+
|
|
457
|
+
top_contexts = [context for context,
|
|
458
|
+
_ in sorted_combined_context_scores[:top_n]]
|
|
459
|
+
logger.debug(f"Final top contexts: {top_contexts}")
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Error retrieving top n context: {e}")
|
|
462
|
+
|
|
463
|
+
return top_contexts
|
|
464
|
+
|
|
465
|
+
class _ContextRelevanceScore(BaseModel):
|
|
466
|
+
score: int = Field(
|
|
467
|
+
...,
|
|
468
|
+
description="This is a Context Relevance Score, ranging from 0 to 100, indicates how well a given coding-related context supports answering a specific question, with higher scores signifying greater relevance."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
async def get_context_relevance_score(self, query: str, context: str) -> int:
|
|
472
|
+
"""
|
|
473
|
+
generate context relevance score based on user's question and provided context
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
query (str): user's initial question.
|
|
477
|
+
context (str): context fetched from Github
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
int: context relevance score, from 0-100.
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
prompt = config.get("scoring_context_prompt")
|
|
484
|
+
response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[context], output_structure=self._ContextRelevanceScore, simple_llm=True)
|
|
485
|
+
logger.debug(
|
|
486
|
+
f"For {context[:20]}, the context relevance score is: {response.score}")
|
|
487
|
+
return response.score
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error(f"Error in get_context_relevance_score: {e}")
|
|
490
|
+
return -1
|
llama_github/utils.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import base64
|
|
3
|
+
import re
|
|
4
|
+
import asyncio
|
|
5
|
+
import aiohttp
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
from llama_github.logger import logger
|
|
8
|
+
|
|
9
|
+
class DataAnonymizer:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.patterns = {
|
|
12
|
+
'api_key': r'(?i)(api[_-]?key|sk[_-]live|sk[_-]test|sk[_-]prod|sk[_-]sandbox|openai[_-]?key)\s*[:=]\s*[\'"]?([a-zA-Z0-9-_]{20,})[\'"]?',
|
|
13
|
+
'token': r'(?i)(token|access[_-]?token|auth[_-]?token|github[_-]?token|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghu_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36})\s*[:=]\s*[\'"]?([a-zA-Z0-9-_]{20,})[\'"]?',
|
|
14
|
+
'password': r'(?i)password\s*[:=]\s*[\'"]?([^\'"]+)[\'"]?',
|
|
15
|
+
'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
|
|
16
|
+
'ip_address': r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b',
|
|
17
|
+
'jwt': r'eyJ[a-zA-Z0-9-_]+\.eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+',
|
|
18
|
+
'phone_number': r'\+?[0-9]{1,4}?[-.\s]?(\(?\d{1,3}?\)?[-.\s]?)?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}',
|
|
19
|
+
'url': r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+',
|
|
20
|
+
'credit_card': r'\b(?:\d[ -]*?){13,16}\b',
|
|
21
|
+
'ssn': r'\b(?:\d[ -]*?){9}\b',
|
|
22
|
+
'ipv6': r'(?i)([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:)',
|
|
23
|
+
'mac_address': r'(?i)([0-9a-f]{2}([:-]|$)){6}',
|
|
24
|
+
'latitude_longitude': r'(?i)(lat|latitude|lon|longitude)\s*[:=]\s*[-+]?([0-9]*\.[0-9]+|[0-9]+),\s*[-+]?([0-9]*\.[0-9]+|[0-9]+)',
|
|
25
|
+
'driver_license': r'(?i)([A-Z0-9]{1,20})\s*[:=]\s*([A-Z0-9]{1,20})',
|
|
26
|
+
'date_of_birth': r'(?i)(dob|date[_-]?of[_-]?birth)\s*[:=]\s*([0-9]{4}-[0-9]{2}-[0-9]{2})',
|
|
27
|
+
'name': r'(?i)(name|first[_-]?name|last[_-]?name)\s*[:=]\s*([a-zA-Z]{2,})',
|
|
28
|
+
'address': r'(?i)(address|street[_-]?address)\s*[:=]\s*([a-zA-Z0-9\s,]{10,})',
|
|
29
|
+
'zipcode': r'(?i)(zip|zipcode)\s*[:=]\s*([0-9]{5})',
|
|
30
|
+
'company': r'(?i)(company|organization)\s*[:=]\s*([a-zA-Z\s]{2,})',
|
|
31
|
+
'job_title': r'(?i)(job[_-]?title)\s*[:=]\s*([a-zA-Z\s]{2,})',
|
|
32
|
+
'domain': r'(?i)(domain)\s*[:=]\s*([a-zA-Z0-9.-]{2,})',
|
|
33
|
+
'hostname': r'(?i)(hostname)\s*[:=]\s*([a-zA-Z0-9.-]{2,})',
|
|
34
|
+
'port': r'(?i)(port)\s*[:=]\s*([0-9]{2,})',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def hash_replacement(match):
|
|
38
|
+
sensitive_data = match.group(0)
|
|
39
|
+
hash_object = hashlib.sha256(sensitive_data.encode())
|
|
40
|
+
hashed_data = base64.urlsafe_b64encode(
|
|
41
|
+
hash_object.digest()).decode('utf-8')
|
|
42
|
+
return f'<ANONYMIZED:{hashed_data[:8]}>'
|
|
43
|
+
|
|
44
|
+
def anonymize_sensitive_data(self, question):
|
|
45
|
+
anonymized_question = question
|
|
46
|
+
for pattern_name, pattern in self.patterns.items():
|
|
47
|
+
anonymized_question = re.sub(
|
|
48
|
+
pattern, self.hash_replacement, anonymized_question)
|
|
49
|
+
return anonymized_question
|
|
50
|
+
|
|
51
|
+
class AsyncHTTPClient:
|
|
52
|
+
"""
|
|
53
|
+
Asynchronous HTTP client class for sending asynchronous HTTP requests.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
async def request(
|
|
58
|
+
url: str,
|
|
59
|
+
method: str = "GET",
|
|
60
|
+
headers: Optional[Dict[str, str]] = None,
|
|
61
|
+
data: Optional[Dict[str, Any]] = None,
|
|
62
|
+
retry_count: int = 1,
|
|
63
|
+
retry_delay: int = 1,
|
|
64
|
+
) -> Optional[aiohttp.ClientResponse]:
|
|
65
|
+
"""
|
|
66
|
+
Send an asynchronous HTTP request.
|
|
67
|
+
|
|
68
|
+
:param url: The URL to send the request to.
|
|
69
|
+
:param method: The HTTP request method, default is "GET".
|
|
70
|
+
:param headers: The request headers, default is None.
|
|
71
|
+
:param data: The request data, default is None.
|
|
72
|
+
:param retry_count: The number of retries, default is 1.
|
|
73
|
+
:param retry_delay: The delay in seconds between each retry, default is 1.
|
|
74
|
+
:return: The response object if the request is successful, otherwise None.
|
|
75
|
+
"""
|
|
76
|
+
async with aiohttp.ClientSession() as session:
|
|
77
|
+
for attempt in range(retry_count):
|
|
78
|
+
try:
|
|
79
|
+
async with session.request(
|
|
80
|
+
method, url, headers=headers, json=data
|
|
81
|
+
) as response:
|
|
82
|
+
if response.status == 200:
|
|
83
|
+
return await response.json()
|
|
84
|
+
else:
|
|
85
|
+
logger.error(
|
|
86
|
+
f"Request failed with status code: {response.status}. "
|
|
87
|
+
f"Retrying ({attempt + 1}/{retry_count})..."
|
|
88
|
+
)
|
|
89
|
+
except aiohttp.ClientError as e:
|
|
90
|
+
logger.error(
|
|
91
|
+
f"Request failed with error: {str(e)}. "
|
|
92
|
+
f"Retrying ({attempt + 1}/{retry_count})..."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if attempt < retry_count - 1:
|
|
96
|
+
await asyncio.sleep(retry_delay)
|
|
97
|
+
|
|
98
|
+
return None
|
llama_github/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|