-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllm.py
More file actions
365 lines (277 loc) Β· 11.4 KB
/
Copy pathllm.py
File metadata and controls
365 lines (277 loc) Β· 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
"""
@author: Raja CSP Raman
source:
?
"""
import os
import requests
import re
from abc import ABC, abstractmethod
from dotenv import load_dotenv
from bs4 import BeautifulSoup
# Load environment variables from .env file
load_dotenv()
# Adapter Pattern - Abstract base class for LLM adapters
class LLMAdapter(ABC):
"""Abstract adapter for different LLM providers"""
@abstractmethod
def get_client(self):
"""Return configured LLM client"""
pass
# Concrete Adapters for different LLM providers
class OllamaAdapter(LLMAdapter):
"""Adapter for Ollama LLM provider"""
def get_client(self):
from langchain_ollama import ChatOllama
model = os.getenv("OLLAMA_MODEL", "llama3.2:3b")
return ChatOllama(
model=model,
temperature=0.7,
num_predict=512,
timeout=120 # Increased timeout to 120 seconds
)
class OpenAIAdapter(LLMAdapter):
"""Adapter for OpenAI LLM provider"""
def get_client(self):
from langchain_openai import ChatOpenAI
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY environment variable is not set")
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
return ChatOpenAI(
api_key=api_key,
model=model,
temperature=0.7,
max_tokens=512,
timeout=120 # Increased timeout to 120 seconds
)
class LlamaCppAdapter(LLMAdapter):
"""Adapter for llama.cpp LLM provider (direct HTTP, no langchain-openai dependency)"""
def __init__(self, base_url="http://127.0.0.1:8080/v1"):
self.base_url = base_url
def _check_server_health(self):
"""Check if llama.cpp server is running"""
try:
# Try to reach the health endpoint or models endpoint
health_url = self.base_url.replace('/v1', '/health')
response = requests.get(health_url, timeout=5)
return response.status_code == 200
except requests.exceptions.RequestException:
try:
# Fallback: try the models endpoint
models_url = f"{self.base_url}/models"
response = requests.get(models_url, timeout=5)
return response.status_code == 200
except requests.exceptions.RequestException:
return False
def get_client(self):
"""Return a simple HTTP client wrapper for llama.cpp"""
if not self._check_server_health():
raise ConnectionError(f"llama.cpp server is not running at {self.base_url}. Please start your llama.cpp server first.")
# Return a simple wrapper that mimics langchain interface
return LlamaCppClient(self.base_url)
class LlamaCppClient:
"""Simple HTTP client for llama.cpp that mimics langchain interface"""
def __init__(self, base_url):
self.base_url = base_url
def invoke(self, prompt):
"""Send request to llama.cpp server"""
try:
response = requests.post(
f"{self.base_url}/chat/completions",
json={
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.7,
"max_tokens": 512
},
timeout=180
)
response.raise_for_status()
data = response.json()
# Extract content from response
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
# Return object with content attribute (like langchain)
class Response:
def __init__(self, content):
self.content = content
return Response(content)
except Exception as e:
raise Exception(f"llama.cpp request failed: {e}")
class GeminiAdapter(LLMAdapter):
"""Adapter for Google Gemini LLM provider"""
def get_client(self):
from langchain_google_genai import ChatGoogleGenerativeAI
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key:
raise ValueError("GOOGLE_API_KEY environment variable is not set")
model = os.getenv("GEMINI_MODEL", "gemini-1.5-flash")
return ChatGoogleGenerativeAI(
google_api_key=api_key,
model=model,
temperature=0.7,
max_tokens=512,
timeout=120
)
# Factory Pattern - Creates appropriate LLM adapter based on provider
class LLMFactory:
"""Factory for creating LLM adapters based on provider type"""
_adapters = {
"ollama": OllamaAdapter,
"openai": OpenAIAdapter,
"llama.cpp": LlamaCppAdapter,
"gemini": GeminiAdapter,
}
@classmethod
def create_adapter(cls, provider: str) -> LLMAdapter:
"""Create and return appropriate LLM adapter"""
provider = provider.lower().strip()
if provider not in cls._adapters:
available_providers = ", ".join(cls._adapters.keys())
raise ValueError(f"Unsupported LLM provider: {provider}. Available providers: {available_providers}")
return cls._adapters[provider]()
@classmethod
def register_adapter(cls, provider: str, adapter_class: type):
"""Register a new LLM adapter (for extensibility)"""
cls._adapters[provider] = adapter_class
def get_llm():
"""Get LLM client based on environment configuration"""
provider = os.getenv("LLM_PROVIDER", "ollama")
print(f"Using LLM provider: {provider}")
adapter = LLMFactory.create_adapter(provider)
print(f"Successfully created adapter for: {provider}")
return adapter.get_client()
# Tool Calling Functions
def detect_url_in_instructions(instructions: str) -> str:
"""
Detect if instructions contain a URL
Args:
instructions: User instructions text
Returns:
URL string if found, None otherwise
"""
if not instructions:
return None
url_pattern = r'https?://[^\s]+'
match = re.search(url_pattern, instructions)
return match.group(0) if match else None
def fetch_content_from_url(url: str) -> dict:
"""
Fetch and parse content from URL using BeautifulSoup
Args:
url: URL to fetch content from
Returns:
dict with 'success', 'content', 'error' keys
"""
try:
print(f"π‘ Fetching content from: {url}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, timeout=15, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# Remove unwanted elements
for element in soup(["script", "style", "nav", "footer", "header", "aside", "iframe"]):
element.decompose()
# Try to find main content areas
content_areas = soup.find_all(
['article', 'main', 'div'],
class_=lambda x: x and any(keyword in x.lower() for keyword in ['content', 'article', 'post', 'entry', 'body'])
)
if not content_areas:
# Fallback to body or entire document
content_areas = [soup.body] if soup.body else [soup]
text_content = []
for area in content_areas:
# Extract headings
for heading in area.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
heading_text = heading.get_text(strip=True)
if heading_text:
text_content.append(f"\n## {heading_text}\n")
# Extract paragraphs
for para in area.find_all('p'):
para_text = para.get_text(strip=True)
if para_text and len(para_text) > 10: # Filter out very short paragraphs
text_content.append(para_text)
# Extract code blocks
for code in area.find_all(['pre', 'code']):
code_text = code.get_text(strip=True)
if code_text:
text_content.append(f"\n```\n{code_text}\n```\n")
# Extract list items
for ul in area.find_all(['ul', 'ol']):
for li in ul.find_all('li', recursive=False):
li_text = li.get_text(strip=True)
if li_text:
text_content.append(f"β’ {li_text}")
# Join and clean content
full_content = "\n".join(text_content)
# Remove excessive whitespace
full_content = re.sub(r'\n{3,}', '\n\n', full_content)
full_content = full_content.strip()
if not full_content or len(full_content) < 100:
return {
'success': False,
'content': None,
'error': 'Insufficient content extracted from URL'
}
print(f"β
Successfully fetched {len(full_content)} characters")
return {
'success': True,
'content': full_content,
'error': None
}
except requests.exceptions.Timeout:
error_msg = f"Timeout while fetching URL: {url}"
print(f"β {error_msg}")
return {'success': False, 'content': None, 'error': error_msg}
except requests.exceptions.RequestException as e:
error_msg = f"Request error: {str(e)}"
print(f"β {error_msg}")
return {'success': False, 'content': None, 'error': error_msg}
except Exception as e:
error_msg = f"Error parsing content: {str(e)}"
print(f"β {error_msg}")
return {'success': False, 'content': None, 'error': error_msg}
def process_instructions_with_url(instructions: str) -> dict:
"""
Process instructions and fetch content if URL is detected
Args:
instructions: User instructions text
Returns:
dict with 'has_url', 'url', 'content', 'enhanced_instructions' keys
"""
url = detect_url_in_instructions(instructions)
if not url:
return {
'has_url': False,
'url': None,
'content': None,
'enhanced_instructions': instructions
}
print(f"π URL detected in instructions: {url}")
result = fetch_content_from_url(url)
if result['success']:
# Limit content size for LLM context (keep first 3000 chars)
content = result['content']
if len(content) > 3000:
content = content[:3000] + "\n\n[Content truncated for length...]"
enhanced_instructions = f"""Content fetched from: {url}
KNOWLEDGE BASE CONTENT:
{content}
Generate quiz questions based on the above content."""
return {
'has_url': True,
'url': url,
'content': result['content'],
'enhanced_instructions': enhanced_instructions
}
else:
print(f"β οΈ Failed to fetch content: {result['error']}")
print(f"π Using original instructions")
return {
'has_url': True,
'url': url,
'content': None,
'enhanced_instructions': instructions
}