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,348 @@
|
|
|
1
|
+
from llama_github.logger import logger
|
|
2
|
+
from llama_github.config.config import config
|
|
3
|
+
from typing import List, Optional, Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
|
|
7
|
+
from llama_github.llm_integration.initial_load import LLMManager
|
|
8
|
+
from llama_github.llm_integration.llm_handler import LLMHandler
|
|
9
|
+
from llama_github.rag_processing.rag_processor import RAGProcessor
|
|
10
|
+
|
|
11
|
+
from llama_github.github_integration.github_auth_manager import GitHubAuthManager
|
|
12
|
+
from llama_github.data_retrieval.github_api import GitHubAPIHandler
|
|
13
|
+
from llama_github.data_retrieval.github_entities import Repository, RepositoryPool
|
|
14
|
+
from llama_github.utils import AsyncHTTPClient
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from IPython import get_ipython
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
19
|
+
from urllib.parse import quote
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class GitHubAppCredentials:
|
|
24
|
+
app_id: int
|
|
25
|
+
private_key: str
|
|
26
|
+
installation_id: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GithubRAG:
|
|
30
|
+
rag_processor: RAGProcessor = None
|
|
31
|
+
|
|
32
|
+
def __init__(self, github_access_token: Optional[str] = None,
|
|
33
|
+
github_app_credentials: Optional[GitHubAppCredentials] = None,
|
|
34
|
+
openai_api_key: Optional[str] = None,
|
|
35
|
+
huggingface_token: Optional[str] = None,
|
|
36
|
+
jina_api_key: Optional[str] = None,
|
|
37
|
+
open_source_models_hg_dir: Optional[str] = None,
|
|
38
|
+
embedding_model: Optional[str] = config.get(
|
|
39
|
+
"default_embedding"),
|
|
40
|
+
rerank_model: Optional[str] = config.get("default_reranker"),
|
|
41
|
+
llm: Any = None,
|
|
42
|
+
**kwargs) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize the GithubRAG with the provided credentials and configuration.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
- github_access_token (Optional[str]): GitHub access token for authentication.
|
|
48
|
+
- github_app_credentials (Optional[GitHubAppCredentials]): Credentials for GitHub App authentication.
|
|
49
|
+
- openai_api_key (Optional[str]): API key for OpenAI services -- recommend to use, GPT-4-turbo will be used.
|
|
50
|
+
- huggingface_token (Optional[str]): Token for Hugging Face services -- recommend to fill.
|
|
51
|
+
- jina_api_key (Optional[str]): API key for Jina AI services -- s.jina.ai API will be used
|
|
52
|
+
- open_source_models_hg_dir (Optional[str]): Name of open-source models from Hugging Face to replace OpenAI.
|
|
53
|
+
- embedding_model (Optional[str]): Name of Embedding model from Hugging Face, if you have preferred embedding model to be used.
|
|
54
|
+
- rerank_model (Optional[str]): Name of Rerank model from Hugging Face, if you have preferred rerank model to be used.
|
|
55
|
+
- llm (Any): Any kind of LangChain llm chat object - to replace OpenAI or open-source models from Hugging Face.
|
|
56
|
+
- **kwargs:
|
|
57
|
+
:param repo_cleanup_interval (Optional[int]): How often to run repo cleanup in seconds within RepositoryPool.
|
|
58
|
+
:param repo_max_idle_time (Optional[int]): Keep a repo in cache until max idle time if not used.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
- None
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
logger.info("Initializing GithubRAG...")
|
|
65
|
+
logger.debug("Initializing Github Instance...")
|
|
66
|
+
|
|
67
|
+
self.auth_manager = GitHubAuthManager()
|
|
68
|
+
if github_access_token:
|
|
69
|
+
self.github_instance = self.auth_manager.authenticate_with_token(
|
|
70
|
+
github_access_token)
|
|
71
|
+
elif github_app_credentials:
|
|
72
|
+
self.github_instance = self.auth_manager.authenticate_with_app(
|
|
73
|
+
github_app_credentials.app_id, github_app_credentials.private_key, github_app_credentials.installation_id)
|
|
74
|
+
else:
|
|
75
|
+
logger.error("GitHub credentials not provided.")
|
|
76
|
+
logger.debug("Github Instance Initialized.")
|
|
77
|
+
|
|
78
|
+
logger.debug("Initializing Repository Pool...")
|
|
79
|
+
param_mapping = {
|
|
80
|
+
"repo_cleanup_interval": "cleanup_interval",
|
|
81
|
+
"repo_max_idle_time": "max_idle_time"
|
|
82
|
+
}
|
|
83
|
+
repo_pool_kwargs = {
|
|
84
|
+
param_mapping[k]: v for k, v in kwargs.items() if k in param_mapping}
|
|
85
|
+
self.RepositoryPool = RepositoryPool(
|
|
86
|
+
self.github_instance, **repo_pool_kwargs)
|
|
87
|
+
self.github_api_handler = GitHubAPIHandler(self.github_instance)
|
|
88
|
+
logger.debug("Repository Pool Initialized.")
|
|
89
|
+
|
|
90
|
+
self.jina_api_key = jina_api_key
|
|
91
|
+
|
|
92
|
+
logger.debug(
|
|
93
|
+
"Initializing llm manager, embedding model & reranker model...")
|
|
94
|
+
self.llm_manager = LLMManager(
|
|
95
|
+
openai_api_key, huggingface_token, open_source_models_hg_dir, embedding_model, rerank_model, llm)
|
|
96
|
+
logger.debug(
|
|
97
|
+
"LLM Manager, Embedding model & Reranker model Initialized.")
|
|
98
|
+
|
|
99
|
+
self.rag_processor = RAGProcessor(
|
|
100
|
+
self.github_api_handler, self.llm_manager)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Error initializing GithubRAG: {e}")
|
|
103
|
+
raise e
|
|
104
|
+
|
|
105
|
+
async def async_retrieve_context(self, query, simple_mode=False) -> List[str]:
|
|
106
|
+
# Implementation of the context retrieval process
|
|
107
|
+
# This will involve using the GitHub API to search for relevant information,
|
|
108
|
+
# augmenting the retrieved data through the RAG methodology, and
|
|
109
|
+
# enhancing it with LLM capabilities.
|
|
110
|
+
|
|
111
|
+
topn_contexts = [] # This will be the list of context strings
|
|
112
|
+
try:
|
|
113
|
+
logger.info("Retrieving context...")
|
|
114
|
+
if simple_mode:
|
|
115
|
+
#In simple mode, only a Google search will be conducted based on the user's question.
|
|
116
|
+
#This model is not suitable for long questions (e.g., questions with more than 20 words).
|
|
117
|
+
task_google_search = asyncio.create_task(
|
|
118
|
+
self.google_search_retrieval(query=query))
|
|
119
|
+
await asyncio.gather(task_google_search)
|
|
120
|
+
logger.debug(
|
|
121
|
+
f"Google search: {str(len(task_google_search.result()))}")
|
|
122
|
+
context_list = self.rag_processor.arrange_context(
|
|
123
|
+
google_search_result=task_google_search.result())
|
|
124
|
+
if len(context_list)>0:
|
|
125
|
+
topn_contexts = await self.rag_processor.retrieve_topn_contexts(
|
|
126
|
+
context_list=context_list, query=query, top_n=config.get("top_n_contexts"))
|
|
127
|
+
else:
|
|
128
|
+
# Analyzing question and generating strategy
|
|
129
|
+
analyze_strategy = asyncio.create_task(
|
|
130
|
+
self.rag_processor.analyze_question(query))
|
|
131
|
+
# wait for generate analyze strategy
|
|
132
|
+
await analyze_strategy
|
|
133
|
+
analyzed_strategy = analyze_strategy.result()
|
|
134
|
+
logger.debug(f"Analyze strategy: {analyzed_strategy}")
|
|
135
|
+
|
|
136
|
+
# google search from GitHub
|
|
137
|
+
tokens = self.llm_manager.get_tokenizer().encode(query)
|
|
138
|
+
query_tokens = len(tokens)
|
|
139
|
+
task_google_search = asyncio.create_task(
|
|
140
|
+
self.google_search_retrieval(query=query if query_tokens < 20 else analyzed_strategy[0]))
|
|
141
|
+
# code search from GitHub
|
|
142
|
+
task_code_search = asyncio.create_task(
|
|
143
|
+
self.code_search_retrieval(query=analyzed_strategy[0], draft_answer=analyzed_strategy[1]+"\n\n"+analyzed_strategy[2]))
|
|
144
|
+
# issue search from GitHub
|
|
145
|
+
task_issue_search = asyncio.create_task(
|
|
146
|
+
self.issue_search_retrieval(query=analyzed_strategy[0], draft_answer=analyzed_strategy[1]+"\n\n"+analyzed_strategy[3]))
|
|
147
|
+
# repo search from GitHub
|
|
148
|
+
task_repo_search = asyncio.create_task(
|
|
149
|
+
self.repo_search_retrieval(query=analyzed_strategy[0]))
|
|
150
|
+
|
|
151
|
+
# wait for all tasks to complete
|
|
152
|
+
await asyncio.gather(task_google_search, task_code_search, task_issue_search, task_repo_search)
|
|
153
|
+
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"Google search: {str(len(task_google_search.result()))}")
|
|
156
|
+
logger.debug(
|
|
157
|
+
f"Code search: {str(len(task_code_search.result()))}")
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Issue search: {str(len(task_issue_search.result()))}")
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Repo search: {str(len(task_repo_search.result()))}")
|
|
162
|
+
|
|
163
|
+
context_list = self.rag_processor.arrange_context(
|
|
164
|
+
code_search_result=task_code_search.result(),
|
|
165
|
+
issue_search_result=task_issue_search.result(),
|
|
166
|
+
repo_search_result=task_repo_search.result(),
|
|
167
|
+
google_search_result=task_google_search.result())
|
|
168
|
+
|
|
169
|
+
if len(context_list)>0:
|
|
170
|
+
topn_contexts = await self.rag_processor.retrieve_topn_contexts(
|
|
171
|
+
context_list=context_list, query=query, answer=analyzed_strategy[1], top_n=config.get("top_n_contexts"))
|
|
172
|
+
|
|
173
|
+
logger.info("Context retrieved successfully.")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error retrieving context: {e}")
|
|
176
|
+
raise e
|
|
177
|
+
return topn_contexts
|
|
178
|
+
|
|
179
|
+
def retrieve_context(self, query, simple_mode=False) -> List[str]:
|
|
180
|
+
"""
|
|
181
|
+
Retrieve context from GitHub code, issue and repo search based on the input query.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
query (str): The query or question to retrieve context for.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List[str]: A list of context strings retrieved from the specified GitHub repositories.
|
|
188
|
+
"""
|
|
189
|
+
self.loop = asyncio.get_event_loop()
|
|
190
|
+
ipython = get_ipython()
|
|
191
|
+
if ipython and ipython.has_trait('kernel'):
|
|
192
|
+
logger.debug("Running in Jupyter notebook, nest_asyncio applied.")
|
|
193
|
+
import nest_asyncio
|
|
194
|
+
nest_asyncio.apply()
|
|
195
|
+
return asyncio.run(self.async_retrieve_context(query, simple_mode=simple_mode))
|
|
196
|
+
if self.loop.is_running():
|
|
197
|
+
return asyncio.ensure_future(self.async_retrieve_context(query, simple_mode=simple_mode))
|
|
198
|
+
return self.loop.run_until_complete(self.async_retrieve_context(query, simple_mode=simple_mode))
|
|
199
|
+
|
|
200
|
+
async def code_search_retrieval(self, query, draft_answer: Optional[str] = None):
|
|
201
|
+
result = []
|
|
202
|
+
try:
|
|
203
|
+
logger.info("Retrieving code search...")
|
|
204
|
+
search_criterias = await self.rag_processor.get_code_search_criteria(query, draft_answer)
|
|
205
|
+
for search_criteria in search_criterias:
|
|
206
|
+
single_search_result = self.github_api_handler.search_code(
|
|
207
|
+
search_criteria.replace('"', ''))
|
|
208
|
+
for d in single_search_result:
|
|
209
|
+
result.append(d)
|
|
210
|
+
# deduplicate results
|
|
211
|
+
seen = set()
|
|
212
|
+
unique_list = []
|
|
213
|
+
for d in result:
|
|
214
|
+
value = d["url"]
|
|
215
|
+
if value not in seen and d["stargazers_count"] + config.get("code_search_max_hits") - d["index"] >= config.get("min_stars_to_keep_result"):
|
|
216
|
+
seen.add(value)
|
|
217
|
+
unique_list.append(d)
|
|
218
|
+
result = unique_list
|
|
219
|
+
|
|
220
|
+
logger.info("Code search retrieved successfully.")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Error retrieving code search: {e}")
|
|
223
|
+
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
async def issue_search_retrieval(self, query, draft_answer: Optional[str] = None):
|
|
227
|
+
result = []
|
|
228
|
+
try:
|
|
229
|
+
logger.info("Retrieving issue search...")
|
|
230
|
+
search_criterias = await self.rag_processor.get_issue_search_criteria(query, draft_answer)
|
|
231
|
+
for search_criteria in search_criterias:
|
|
232
|
+
single_search_result = self.github_api_handler.search_issues(
|
|
233
|
+
search_criteria.replace('"', ''))
|
|
234
|
+
for d in single_search_result:
|
|
235
|
+
result.append(d)
|
|
236
|
+
# deduplicate results
|
|
237
|
+
seen = set()
|
|
238
|
+
unique_list = []
|
|
239
|
+
for d in result:
|
|
240
|
+
value = d["url"]
|
|
241
|
+
if value not in seen:
|
|
242
|
+
seen.add(value)
|
|
243
|
+
unique_list.append(d)
|
|
244
|
+
result = unique_list
|
|
245
|
+
|
|
246
|
+
logger.info("Issue search retrieved successfully.")
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"Error retrieving issue search: {e}")
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
async def google_search_retrieval(self, query):
|
|
253
|
+
result = []
|
|
254
|
+
try:
|
|
255
|
+
logger.info("Retrieving google search...")
|
|
256
|
+
encoded_query = quote("site:github.com "+query)
|
|
257
|
+
url = f"https://s.jina.ai/{encoded_query}"
|
|
258
|
+
if self.jina_api_key is not None and self.jina_api_key != "":
|
|
259
|
+
headers = {
|
|
260
|
+
"Accept": "application/json",
|
|
261
|
+
"Authorization": f"Bearer {self.jina_api_key}"
|
|
262
|
+
}
|
|
263
|
+
retry_delay = 1
|
|
264
|
+
else:
|
|
265
|
+
headers = {
|
|
266
|
+
"Accept": "application/json"
|
|
267
|
+
}
|
|
268
|
+
retry_delay = 20
|
|
269
|
+
|
|
270
|
+
response = await AsyncHTTPClient.request(url, headers=headers, retry_count=2, retry_delay=retry_delay)
|
|
271
|
+
urls = []
|
|
272
|
+
urls = [item["url"] for item in response["data"] if "url" in item]
|
|
273
|
+
|
|
274
|
+
for github_url in urls:
|
|
275
|
+
content = self.github_api_handler.get_github_url_content(
|
|
276
|
+
github_url)
|
|
277
|
+
if content and content != "":
|
|
278
|
+
result.append({
|
|
279
|
+
'url': github_url,
|
|
280
|
+
'content': content
|
|
281
|
+
})
|
|
282
|
+
logger.info(f"Google search retrieved successfully:{urls}")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Error retrieving google search: {e}")
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
def _get_repository_rag_info(self, repository: Repository):
|
|
288
|
+
"""
|
|
289
|
+
Helper method to get file content through a Repository object.
|
|
290
|
+
|
|
291
|
+
:param code_search_result: A single code search result.
|
|
292
|
+
:return: Tuple containing the Repository object and the file content.
|
|
293
|
+
"""
|
|
294
|
+
# Assuming RepositoryPool is accessible and initialized somewhere in this class
|
|
295
|
+
return repository.get_readme(), self.rag_processor.get_repo_simple_structure(repository)
|
|
296
|
+
|
|
297
|
+
async def repo_search_retrieval(self, query, draft_answer: Optional[str] = None):
|
|
298
|
+
result = []
|
|
299
|
+
results_with_index = []
|
|
300
|
+
try:
|
|
301
|
+
logger.info("Retrieving repo search...")
|
|
302
|
+
search_criterias = await self.rag_processor.get_repo_search_criteria(query, draft_answer)
|
|
303
|
+
for search_criteria in search_criterias:
|
|
304
|
+
single_search_result = self.github_api_handler.search_repositories(
|
|
305
|
+
search_criteria.replace('"', ''))
|
|
306
|
+
for d in single_search_result:
|
|
307
|
+
result.append(d)
|
|
308
|
+
# deduplicate results
|
|
309
|
+
seen = set()
|
|
310
|
+
unique_list = []
|
|
311
|
+
for d in result:
|
|
312
|
+
value = d.full_name
|
|
313
|
+
if value not in seen:
|
|
314
|
+
seen.add(value)
|
|
315
|
+
unique_list.append(d)
|
|
316
|
+
repositories = unique_list
|
|
317
|
+
|
|
318
|
+
with ThreadPoolExecutor(max_workers=config.get("max_workers")) as executor:
|
|
319
|
+
# Concurrently fetch the file content for each code search result
|
|
320
|
+
future_to_index = {executor.submit(
|
|
321
|
+
self._get_repository_rag_info, repository): index for index, repository in enumerate(repositories)}
|
|
322
|
+
for future in as_completed(future_to_index):
|
|
323
|
+
index = future_to_index[future]
|
|
324
|
+
repository = repositories[index]
|
|
325
|
+
try:
|
|
326
|
+
repo_readme, repo_simple_structure = future.result()
|
|
327
|
+
if repo_readme is None or repository.description is None or repository.description == "" or repo_simple_structure == "{}":
|
|
328
|
+
continue
|
|
329
|
+
if repo_readme:
|
|
330
|
+
results_with_index.append({
|
|
331
|
+
'index': index,
|
|
332
|
+
'full_name': repository.full_name,
|
|
333
|
+
'content': repo_readme,
|
|
334
|
+
})
|
|
335
|
+
# if repo_simple_structure:
|
|
336
|
+
# results_with_index.append({
|
|
337
|
+
# 'index': index,
|
|
338
|
+
# 'full_name': repository.full_name,
|
|
339
|
+
# 'content': "The repository "+repository.full_name+" with description:" + repository.description+" has below repo simple structure:\n"+repo_simple_structure,
|
|
340
|
+
# })
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(
|
|
343
|
+
f"Error getting repository info: {e}")
|
|
344
|
+
|
|
345
|
+
logger.info("Repo search retrieved successfully.")
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Error retrieving repos search: {e}")
|
|
348
|
+
return results_with_index
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# initial_load.py
|
|
2
|
+
import torch
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from langchain_openai import ChatOpenAI
|
|
6
|
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
|
7
|
+
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
|
|
8
|
+
|
|
9
|
+
from llama_github.config.config import config
|
|
10
|
+
from llama_github.logger import logger
|
|
11
|
+
|
|
12
|
+
from transformers import AutoModel
|
|
13
|
+
from transformers import AutoModelForSequenceClassification
|
|
14
|
+
from transformers import AutoTokenizer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LLMManager:
|
|
18
|
+
_instance_lock = Lock()
|
|
19
|
+
_instance = None
|
|
20
|
+
llm = None
|
|
21
|
+
# embedding_model = None
|
|
22
|
+
rerank_model = None
|
|
23
|
+
_initialized = False
|
|
24
|
+
llm_simple = None
|
|
25
|
+
tokenizer = None
|
|
26
|
+
|
|
27
|
+
def __new__(cls, *args, **kwargs):
|
|
28
|
+
if cls._instance is None: # First check (unlocked)
|
|
29
|
+
with cls._instance_lock: # Acquire lock
|
|
30
|
+
if cls._instance is None: # Second check (locked)
|
|
31
|
+
cls._instance = super(LLMManager, cls).__new__(cls)
|
|
32
|
+
return cls._instance
|
|
33
|
+
|
|
34
|
+
def __init__(self,
|
|
35
|
+
openai_api_key: Optional[str] = None,
|
|
36
|
+
huggingface_token: Optional[str] = None,
|
|
37
|
+
open_source_models_hg_dir: Optional[str] = None,
|
|
38
|
+
embedding_model: Optional[str] = config.get(
|
|
39
|
+
"default_embedding"),
|
|
40
|
+
rerank_model: Optional[str] = config.get("default_reranker"),
|
|
41
|
+
llm: Any = None):
|
|
42
|
+
with self._instance_lock: # Prevent re-initialization
|
|
43
|
+
if self._initialized:
|
|
44
|
+
return
|
|
45
|
+
self._initialized = True
|
|
46
|
+
|
|
47
|
+
# Initialize for OpenAI GPT-4
|
|
48
|
+
if llm is not None:
|
|
49
|
+
self.llm = llm
|
|
50
|
+
self.model_type = "Custom_langchain_llm"
|
|
51
|
+
elif openai_api_key is not None and openai_api_key != "" and self.llm is None:
|
|
52
|
+
logger.info("Initializing OpenAI API...")
|
|
53
|
+
self.llm = ChatOpenAI(api_key=openai_api_key, model="gpt-4-turbo")
|
|
54
|
+
self.llm_simple = ChatOpenAI(
|
|
55
|
+
api_key=openai_api_key, model="gpt-3.5-turbo")
|
|
56
|
+
self.model_type = "OpenAI"
|
|
57
|
+
# Initialize for Open Source Models
|
|
58
|
+
elif open_source_models_hg_dir is not None and open_source_models_hg_dir != "" and self.llm is None:
|
|
59
|
+
logger.info(f"Initializing {open_source_models_hg_dir}...")
|
|
60
|
+
# load hugingface models
|
|
61
|
+
self.model_type = "Hubgingface"
|
|
62
|
+
elif self.llm is None:
|
|
63
|
+
# default model is phi3_mini_128k
|
|
64
|
+
self.model_type = "Hubgingface"
|
|
65
|
+
|
|
66
|
+
# initial model_kwargs
|
|
67
|
+
if torch.cuda.is_available():
|
|
68
|
+
self.device = torch.device('cuda')
|
|
69
|
+
elif torch.backends.mps.is_available():
|
|
70
|
+
self.device = torch.device('mps')
|
|
71
|
+
else:
|
|
72
|
+
self.device = torch.device('cpu')
|
|
73
|
+
|
|
74
|
+
# initial embedding_model
|
|
75
|
+
if self.tokenizer is None:
|
|
76
|
+
logger.info(f"Initializing {embedding_model}...")
|
|
77
|
+
self.tokenizer = AutoTokenizer.from_pretrained(embedding_model)
|
|
78
|
+
self.embedding_model = AutoModel.from_pretrained(
|
|
79
|
+
embedding_model, trust_remote_code=True).to(self.device)
|
|
80
|
+
|
|
81
|
+
# initial rerank_model
|
|
82
|
+
if self.rerank_model is None:
|
|
83
|
+
logger.info(f"Initializing {rerank_model}...")
|
|
84
|
+
self.rerank_model = AutoModelForSequenceClassification.from_pretrained(
|
|
85
|
+
rerank_model, num_labels=1, trust_remote_code=True
|
|
86
|
+
).to(self.device)
|
|
87
|
+
|
|
88
|
+
def get_llm(self):
|
|
89
|
+
return self.llm
|
|
90
|
+
|
|
91
|
+
def get_llm_simple(self):
|
|
92
|
+
return self.llm_simple
|
|
93
|
+
|
|
94
|
+
def get_tokenizer(self):
|
|
95
|
+
return self.tokenizer
|
|
96
|
+
|
|
97
|
+
def get_rerank_model(self):
|
|
98
|
+
return self.rerank_model
|
|
99
|
+
|
|
100
|
+
def get_embedding_model(self):
|
|
101
|
+
return self.embedding_model
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# llm_handler.py
|
|
2
|
+
# to do list
|
|
3
|
+
# 1. add streaming output for invoke.
|
|
4
|
+
|
|
5
|
+
from llama_github.llm_integration.initial_load import LLMManager
|
|
6
|
+
from langchain_core.prompts import ChatPromptTemplate, ChatMessagePromptTemplate, MessagesPlaceholder
|
|
7
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
8
|
+
from llama_github.config.config import config
|
|
9
|
+
from llama_github.logger import logger
|
|
10
|
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
|
11
|
+
from langchain_core.pydantic_v1 import BaseModel
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from langchain_openai import output_parsers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LLMHandler:
|
|
17
|
+
def __init__(self, llm_manager: Optional[LLMManager] = None):
|
|
18
|
+
"""
|
|
19
|
+
Initializes the LLMHandler class which is responsible for handling the interaction
|
|
20
|
+
with a language model (LLM) using the LangChain framework.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
llm_manager (LLMManager): Manages interactions with the language model.
|
|
24
|
+
"""
|
|
25
|
+
if llm_manager is not None:
|
|
26
|
+
self.llm_manager = llm_manager
|
|
27
|
+
else:
|
|
28
|
+
self.llm_manager = LLMManager()
|
|
29
|
+
|
|
30
|
+
async def ainvoke(self, human_question: str, chat_history: Optional[list[str]] = None, context: Optional[list[str]] = None, output_structure: Optional[BaseModel] = None, prompt: str = config.get("general_prompt"), simple_llm=False) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Asynchronously invokes the language model with a given question, chat history, and context,
|
|
33
|
+
and returns the model's response.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
human_question (str): The question or input from the human user.
|
|
37
|
+
chat_history (list[str]): A list of strings representing the chat history, where each
|
|
38
|
+
string is a message. This parameter is optional.
|
|
39
|
+
context (list[str]): A list of strings representing additional context for the model.
|
|
40
|
+
This parameter is optional.
|
|
41
|
+
output_structure: A langchain_core.pydantic_v1.BaseModel object to control desired
|
|
42
|
+
structure of the output from the language model.
|
|
43
|
+
This parameter is optional and allows for more detailed control over
|
|
44
|
+
the model's responses.
|
|
45
|
+
prompt (str): A template for the prompt to be used with the language model. Defaults
|
|
46
|
+
to a general prompt defined in the configuration.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: The response from the language model.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
if simple_llm and self.llm_manager.get_llm_simple() is not None:
|
|
53
|
+
llm = self.llm_manager.get_llm_simple()
|
|
54
|
+
else:
|
|
55
|
+
llm = self.llm_manager.get_llm()
|
|
56
|
+
if self.llm_manager.model_type == "OpenAI":
|
|
57
|
+
# Create a prompt template with placeholders for dynamic content.
|
|
58
|
+
prompt_template = ChatMessagePromptTemplate.from_template(
|
|
59
|
+
role="system", template=prompt)
|
|
60
|
+
chat_prompt = ChatPromptTemplate.from_messages([
|
|
61
|
+
prompt_template,
|
|
62
|
+
MessagesPlaceholder(
|
|
63
|
+
variable_name="history_messages", optional=True),
|
|
64
|
+
MessagesPlaceholder(variable_name="human_message"),
|
|
65
|
+
MessagesPlaceholder(
|
|
66
|
+
variable_name="context_messages", optional=True)
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
# Convert chat_history and context from [str] to their respective message types.
|
|
70
|
+
chat_history_messages = self._compose_context_messages(
|
|
71
|
+
chat_history)
|
|
72
|
+
context_messages = self._compose_chat_history_messages(context)
|
|
73
|
+
human_question_message = HumanMessage(content=human_question)
|
|
74
|
+
|
|
75
|
+
prompt_params = {
|
|
76
|
+
"history_messages": chat_history_messages,
|
|
77
|
+
"human_message": [human_question_message],
|
|
78
|
+
"context_messages": context_messages
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Format the prompt with the provided parameters.
|
|
82
|
+
formatted_prompt = chat_prompt.format_prompt(**prompt_params)
|
|
83
|
+
logger.debug(f"Formatted prompt: {formatted_prompt}")
|
|
84
|
+
# Determine the processing chain based on the presence of an output structure.
|
|
85
|
+
if output_structure is not None:
|
|
86
|
+
chain = llm.with_structured_output(output_structure)
|
|
87
|
+
else:
|
|
88
|
+
chain = llm
|
|
89
|
+
|
|
90
|
+
# Invoke the chain and return the model's response.
|
|
91
|
+
try:
|
|
92
|
+
response = await chain.ainvoke(formatted_prompt.to_messages())
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.exception(
|
|
95
|
+
f"Call llm with #{human_question}# generated an exception:{e}")
|
|
96
|
+
if output_structure is not None:
|
|
97
|
+
response = await chain.ainvoke(formatted_prompt.to_messages())
|
|
98
|
+
return response
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.exception(
|
|
101
|
+
f"Call llm with #{human_question}# generated an exception:{e}")
|
|
102
|
+
return "An error occurred during processing."
|
|
103
|
+
|
|
104
|
+
def _compose_chat_history_messages(self, chat_history: list[str]) -> list:
|
|
105
|
+
"""
|
|
106
|
+
Converts chat history from a list of strings to a list of alternating HumanMessage
|
|
107
|
+
and AIMessage objects, starting with HumanMessage.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
chat_history (list[str]): The chat history as a list of strings.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list: A list of alternating HumanMessage and AIMessage objects.
|
|
114
|
+
"""
|
|
115
|
+
messages = []
|
|
116
|
+
for i, message in enumerate(chat_history or []):
|
|
117
|
+
message_class = HumanMessage if i % 2 == 0 else AIMessage
|
|
118
|
+
messages.append(message_class(content=message))
|
|
119
|
+
return messages
|
|
120
|
+
|
|
121
|
+
def _compose_context_messages(self, context: list[str]) -> list:
|
|
122
|
+
"""
|
|
123
|
+
Converts context from a list of strings to a list of SystemMessage objects.
|
|
124
|
+
|
|
125
|
+
Parameters:
|
|
126
|
+
context (list[str]): The context as a list of strings.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
list: A list of SystemMessage objects.
|
|
130
|
+
"""
|
|
131
|
+
return [SystemMessage(content=message) for message in context or []]
|
llama_github/logger.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger('llama_github')
|
|
4
|
+
|
|
5
|
+
def configure_logging(level=logging.INFO, handler=None):
|
|
6
|
+
logger.setLevel(level)
|
|
7
|
+
if handler:
|
|
8
|
+
logger.addHandler(handler)
|
|
9
|
+
else:
|
|
10
|
+
# default handler output to console
|
|
11
|
+
ch = logging.StreamHandler()
|
|
12
|
+
ch.setLevel(level)
|
|
13
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
14
|
+
ch.setFormatter(formatter)
|
|
15
|
+
logger.addHandler(ch)
|
|
16
|
+
|
|
17
|
+
## Logging Configuration
|
|
18
|
+
# By default, `llama_github` does not configure logging to avoid interfering with your application's logging settings. If you want to see log outputs from `llama_github`, you can configure logging as follows:
|
|
19
|
+
|
|
20
|
+
# ```python
|
|
21
|
+
# import logging
|
|
22
|
+
# import llama_github
|
|
23
|
+
|
|
24
|
+
# # Configure the library's logger
|
|
25
|
+
# llama_github.configure_logging(level=logging.INFO)
|
|
26
|
+
|
|
27
|
+
# # Now you can see log outputs
|
|
28
|
+
# logger = logging.getLogger('llama_github')
|
|
File without changes
|