-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllm_client.py
More file actions
345 lines (281 loc) Β· 13.4 KB
/
Copy pathllm_client.py
File metadata and controls
345 lines (281 loc) Β· 13.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
"""
LLM client for WikiTalk
"""
import requests
import json
import logging
from typing import List, Dict, Any, Optional
from datetime import datetime
from config import *
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class LLMClient:
def __init__(self):
self.url = LLM_URL
self.model = LLM_MODEL
self.temperature = TEMPERATURE
logger.info(f"π€ LLM Client initialized")
logger.info(f" URL: {self.url}")
logger.info(f" Model: {self.model}")
logger.info(f" Temperature: {self.temperature}")
def query_rewrite(self, query: str, conversation_history: List[Dict[str, str]]) -> str:
"""Rewrite query based on conversation history"""
# OPTIMIZATION: Query rewriting disabled for speed
# Queries are usually specific enough already, and this adds 0.5+ seconds per request
# Set to True to re-enable
QUERY_REWRITE_ENABLED = True
if not QUERY_REWRITE_ENABLED:
return query
logger.debug(f"π Query rewrite requested for: '{query}'")
if not conversation_history:
logger.debug(" No conversation history, returning original query")
return query
# Get recent conversation context
recent_history = conversation_history[-MEMORY_TURNS:]
logger.info(f" π Found {len(conversation_history)} total messages, using last {len(recent_history)}")
# Create context for query rewriting
context_parts = []
for turn in recent_history:
if turn['role'] == 'user':
context_parts.append(f"User: {turn['content'][:100]}")
elif turn['role'] == 'assistant':
context_parts.append(f"Assistant: {turn['content'][:100]}")
context = "\n".join(context_parts)
logger.info(f" π Context: {context[:200]}")
# Rewrite query to be more specific
rewrite_prompt = f"""Task: Rewrite a follow-up question to be complete and specific for Wikipedia search.
Previous messages:
{context}
Current question: {query}
Write the rewritten question as a single sentence. Nothing else. Just the question."""
try:
logger.info(f"π Attempting LLM query rewrite...")
response = self._call_llm(rewrite_prompt, max_tokens=100)
rewritten = response.strip()
# Remove common preamble text that LLMs add
# Split by newline and find the first meaningful line
lines = rewritten.split('\n')
for line in lines:
line = line.strip()
# Skip empty lines and lines that are just metadata
if line and not any(meta in line.lower() for meta in ['question:', 'rewritten', "here's", 'sure,']):
rewritten = line
break
# Also handle inline preambles
common_prefixes = [
"Sure, here's the rewritten",
"Here's the rewritten",
"The rewritten",
"Rewritten:",
]
for prefix in common_prefixes:
if prefix.lower() in rewritten.lower():
idx = rewritten.lower().find(prefix.lower())
if idx >= 0:
remainder = rewritten[idx + len(prefix):].strip()
remainder = remainder.lstrip('*:- ').strip()
if remainder:
rewritten = remainder
break
# Fallback: if LLM returned an error or gave up, just use original
error_phrases = [
"does not provide",
"cannot answer",
"no information",
"i cannot",
"unable to",
]
if any(phrase.lower() in rewritten.lower() for phrase in error_phrases):
logger.info(f" β οΈ LLM got confused, using original query")
return query
logger.info(f" β Query rewritten to: '{rewritten}'")
return rewritten
except Exception as e:
logger.warning(f"β οΈ Query rewrite failed: {e}")
logger.info(f" Using original query instead")
return query
def generate_response(self, query: str, sources: List[Dict[str, Any]],
conversation_history: List[Dict[str, str]]) -> str:
"""Generate response using retrieved sources"""
logger.info(f"π Generating response for query: '{query}'")
logger.info(f" Using {len(sources)} sources")
# Log source summaries for debugging
for i, source in enumerate(sources[:3], 1):
source_preview = source.get('text', '')[:100].replace('\n', ' ')
logger.debug(f" Source {i} ({source.get('title', 'Unknown')}): {source_preview}...")
# Format sources for the prompt
sources_text = self._format_sources_for_prompt(sources)
# Create system prompt
system_prompt = """You are a factual historian using provided Wikipedia sources.
Answer the user's question based on the sources provided.
Cite sources with [1], [2], etc.
If information is missing from the sources, say so clearly.
Be conversational but accurate."""
# Create user prompt
user_prompt = f"""Question: {query}
Sources:
{sources_text}
Answer:"""
# Add conversation context if available
if conversation_history:
recent_history = conversation_history[-MEMORY_TURNS:]
logger.debug(f" Including {len(recent_history)} recent history messages")
context_parts = []
for turn in recent_history:
if turn['role'] == 'user':
context_parts.append(f"Previous question: {turn['content']}")
elif turn['role'] == 'assistant':
context_parts.append(f"Previous answer: {turn['content']}")
if context_parts:
context = "\n".join(context_parts)
user_prompt = f"""Context from previous conversation:
{context}
Current question: {query}
Sources:
{sources_text}
Answer:"""
try:
logger.info(f"π Attempting LLM response generation...")
response = self._call_llm(user_prompt, system_prompt=system_prompt)
logger.info(f" β Response generated ({len(response)} chars)")
return response.strip()
except Exception as e:
logger.error(f"β LLM generation failed: {e}")
fallback = "I apologize, but I'm having trouble generating a response right now."
return fallback
def _format_sources_for_prompt(self, sources: List[Dict[str, Any]]) -> str:
"""Format sources for the LLM prompt"""
formatted_sources = []
for i, source in enumerate(sources, 1):
formatted_sources.append(f"[{i}] {source['title']}\n{source['text']}")
return "\n\n".join(formatted_sources)
def _call_llm(self, prompt: str, system_prompt: Optional[str] = None,
max_tokens: int = 1000) -> str:
"""Call the LLM API"""
logger.debug(f"π Preparing LLM API call")
logger.debug(f" Prompt length: {len(prompt)} chars")
logger.debug(f" Max tokens: {max_tokens}")
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
logger.debug(f" System prompt included ({len(system_prompt)} chars)")
messages.append({"role": "user", "content": prompt})
payload = {
"model": self.model,
"messages": messages,
"temperature": self.temperature,
"max_tokens": max_tokens,
"stream": False
}
try:
logger.info(f"π Sending request to {self.url}")
response = requests.post(
self.url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
logger.debug(f" Status code: {response.status_code}")
result = response.json()
logger.debug(f" Response received")
if 'choices' not in result or not result['choices']:
logger.error(f" No choices in response: {result}")
raise Exception("No response choices in LLM response")
content = result['choices'][0]['message']['content']
logger.info(f" β LLM responded ({len(content)} chars)")
return content
except requests.exceptions.ConnectionError as e:
logger.error(f"β Connection failed to {self.url}: {e}")
logger.error(f" Is LM Studio or llama.cpp running?")
raise Exception(f"Cannot connect to LLM at {self.url}")
except requests.exceptions.Timeout as e:
logger.error(f"β LLM request timed out: {e}")
raise Exception(f"LLM request timed out")
except requests.exceptions.RequestException as e:
logger.error(f"β LLM API call failed: {e}")
raise Exception(f"LLM API call failed: {e}")
except KeyError as e:
logger.error(f"β Unexpected LLM response format: {e}")
logger.error(f" Response: {result}")
raise Exception(f"Unexpected LLM response format: {e}")
class ConversationManager:
def __init__(self):
self.conversations_dir = CONVERSATIONS_DIR
self.conversations_dir.mkdir(exist_ok=True)
logger.info(f"πΎ Conversation Manager initialized")
logger.info(f" Storage: {self.conversations_dir}")
def save_conversation(self, session_id: str, history: List[Dict[str, str]],
last_topic: str = None):
"""Save conversation to file"""
logger.debug(f"πΎ Saving conversation {session_id}")
logger.debug(f" History length: {len(history)} messages")
conversation_data = {
"session_id": session_id,
"history": history,
"last_topic": last_topic,
"timestamp": datetime.now().isoformat()
}
file_path = self.conversations_dir / f"session_{session_id}.json"
try:
with open(file_path, 'w') as f:
json.dump(conversation_data, f, indent=2)
logger.info(f" β Saved to {file_path}")
except Exception as e:
logger.error(f" β Failed to save: {e}")
def load_conversation(self, session_id: str) -> Dict[str, Any]:
"""Load conversation from file"""
logger.debug(f"π Loading conversation {session_id}")
file_path = self.conversations_dir / f"session_{session_id}.json"
if file_path.exists():
try:
with open(file_path, 'r') as f:
data = json.load(f)
logger.info(f" β Loaded {len(data.get('history', []))} messages")
return data
except Exception as e:
logger.error(f" β Failed to load: {e}")
else:
logger.debug(f" New session (no saved conversation)")
return {
"session_id": session_id,
"history": [],
"last_topic": None,
"timestamp": datetime.now().isoformat()
}
def add_exchange(self, session_id: str, user_message: str, assistant_message: str):
"""Add a new exchange to the conversation"""
logger.debug(f"β Adding exchange to {session_id}")
conversation = self.load_conversation(session_id)
conversation["history"].append({"role": "user", "content": user_message})
conversation["history"].append({"role": "assistant", "content": assistant_message})
logger.debug(f" User: {user_message[:50]}...")
logger.debug(f" Assistant: {assistant_message[:50]}...")
# Keep only recent history
if len(conversation["history"]) > MEMORY_TURNS * 2:
logger.debug(f" Trimming history to {MEMORY_TURNS * 2} messages")
conversation["history"] = conversation["history"][-MEMORY_TURNS * 2:]
self.save_conversation(session_id, conversation["history"], conversation.get("last_topic"))
if __name__ == "__main__":
logger.info("π§ͺ Running LLM client tests")
# Test LLM client
client = LLMClient()
# Test query rewrite
logger.info("\nπ Testing query rewrite...")
history = [
{"role": "user", "content": "Tell me about World War I"},
{"role": "assistant", "content": "World War I was a global war..."}
]
rewritten = client.query_rewrite("What caused it?", history)
logger.info(f"Original: 'What caused it?'")
logger.info(f"Rewritten: '{rewritten}'")
# Test conversation manager
logger.info("\nπΎ Testing conversation manager...")
manager = ConversationManager()
manager.add_exchange("test_session", "Hello", "Hi there!")
conversation = manager.load_conversation("test_session")
logger.info(f"Conversation has {len(conversation['history'])} messages")