mtyrrell commited on
Commit
6a14896
·
1 Parent(s): c997974

geolocation + session tracking to log

Browse files
.gitignore CHANGED
@@ -1,8 +1,13 @@
1
- .DS_store
2
  .env
3
  /testing/
4
  /logs/
5
  logging_config.py
6
  /data/
7
  app_interactions.jsonl
8
- auditqa/__pycache__/
 
 
 
 
 
 
1
+ .DS_Store
2
  .env
3
  /testing/
4
  /logs/
5
  logging_config.py
6
  /data/
7
  app_interactions.jsonl
8
+
9
+ # Python cache files
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ .pytest_cache/
app.py CHANGED
@@ -4,7 +4,7 @@ import logging
4
  import asyncio
5
  import os
6
  from uuid import uuid4
7
- from datetime import datetime
8
  from pathlib import Path
9
  from huggingface_hub import CommitScheduler
10
  from auditqa.sample_questions import QUESTIONS
@@ -12,17 +12,20 @@ from auditqa.reports import files, report_list, new_files, new_report_list
12
  from auditqa.process_chunks import load_chunks, getconfig, get_local_qdrant, load_new_chunks
13
  from auditqa.retriever import get_context
14
  from auditqa.reader import nvidia_client, dedicated_endpoint
15
- from auditqa.utils import make_html_source, parse_output_llm_with_sources, save_logs, get_message_template
16
  from dotenv import load_dotenv
17
  from threading import Lock
18
- import json
19
- from functools import partial
 
 
20
 
21
  # TESTING DEBUG LOG
22
  from auditqa.logging_config import setup_logging
23
  setup_logging()
24
  import logging
25
  logger = logging.getLogger(__name__)
 
26
 
27
  load_dotenv()
28
 
@@ -99,11 +102,57 @@ def submit_feedback(feedback, logs_data):
99
  # Still need to return the expected outputs even on error
100
  return gr.update(visible=False), gr.update(visible=True)
101
 
102
- async def chat(query,history,sources,reports,subtype,year):
103
- """taking a query and a message history, use a pipeline (reformulation, retriever, answering)
104
- to yield a tuple of:(messages in gradio format/messages in langchain format, source documents)
105
- """
106
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  print(f">> NEW QUESTION : {query}")
108
  print(f"history:{history}")
109
  print(f"sources:{sources}")
@@ -158,9 +207,12 @@ async def chat(query,history,sources,reports,subtype,year):
158
 
159
  ##-----------------------get answer from endpoints------------------------------
160
  answer_yet = ""
161
- # Create logs data structure at the beginning (so that feedback can be saved after streaming
162
  timestamp = str(datetime.now().timestamp())
163
  logs_data = {
 
 
 
164
  "system_prompt": SYSTEM_PROMPT,
165
  "sources": sources,
166
  "reports": reports,
@@ -171,7 +223,7 @@ async def chat(query,history,sources,reports,subtype,year):
171
  "endpoint_type": model_config.get('reader','TYPE'),
172
  "reader": model_config.get('reader','NVIDIA_MODEL'),
173
  "docs": [doc.page_content for doc in context_retrieved],
174
- "answer": "", # Updated after streaming
175
  "time": timestamp,
176
  }
177
 
@@ -196,14 +248,14 @@ async def chat(query,history,sources,reports,subtype,year):
196
  history[-1] = (query, parsed_answer)
197
  # Update logs_data with current answer
198
  logs_data["answer"] = parsed_answer
199
- yield [tuple(x) for x in history], docs_html, logs_data
200
 
201
  # Stream the response updates
202
  async for update in process_stream():
203
  yield update
204
 
205
  else:
206
- chat_model = dedicated_endpoint() # TESTING: ADAPTED FOR HF INFERENCE API
207
  async def process_stream():
208
  nonlocal answer_yet
209
  try:
@@ -228,7 +280,7 @@ async def chat(query,history,sources,reports,subtype,year):
228
  history[-1] = (query, parsed_answer)
229
  # Update logs_data with current answer
230
  logs_data["answer"] = parsed_answer
231
- yield [tuple(x) for x in history], docs_html, logs_data
232
  await asyncio.sleep(0.05)
233
 
234
  except Exception as e:
@@ -482,24 +534,36 @@ with gr.Blocks(title="Audit Q&A", css= "style.css", theme=theme,elem_id = "main-
482
 
483
  #-------------------- Gradio voodoo continued -------------------------
484
 
485
- # Using event listeners for 1. query box 2. click on example question
486
- # https://www.gradio.app/docs/gradio/textbox#event-listeners-arguments
 
 
 
 
 
 
 
 
487
  (textbox
488
- .submit(start_chat, [textbox, chatbot], [textbox, tabs, chatbot], queue=False, api_name="start_chat_textbox")
489
- # queue must be set as False (default) so the process is not waiting for another to be finished
490
- .then(chat, [textbox, chatbot, dropdown_sources, dropdown_reports, dropdown_category, dropdown_year], [chatbot, sources_textbox, feedback_state], queue=True, concurrency_limit=8, api_name="chat_textbox")
 
 
 
491
  .then(show_feedback, [feedback_state], [feedback_row, feedback_thanks, feedback_state], api_name="show_feedback_textbox")
492
  .then(finish_chat, None, [textbox], api_name="finish_chat_textbox"))
493
 
494
  (examples_hidden
495
  .change(start_chat, [examples_hidden, chatbot], [textbox, tabs, chatbot], queue=False, api_name="start_chat_examples")
496
- # queue must be set as False (default) so the process is not waiting for another to be finished
497
- .then(chat, [examples_hidden, chatbot, dropdown_sources, dropdown_reports, dropdown_category, dropdown_year], [chatbot, sources_textbox, feedback_state], queue=True, concurrency_limit=8, api_name="chat_examples")
 
 
 
498
  .then(show_feedback, [feedback_state], [feedback_row, feedback_thanks, feedback_state], api_name="show_feedback_examples")
499
  .then(finish_chat, None, [textbox], api_name="finish_chat_examples"))
500
 
501
-
502
-
503
  demo.queue()
504
 
505
  demo.launch()
 
4
  import asyncio
5
  import os
6
  from uuid import uuid4
7
+ from datetime import datetime, timedelta
8
  from pathlib import Path
9
  from huggingface_hub import CommitScheduler
10
  from auditqa.sample_questions import QUESTIONS
 
12
  from auditqa.process_chunks import load_chunks, getconfig, get_local_qdrant, load_new_chunks
13
  from auditqa.retriever import get_context
14
  from auditqa.reader import nvidia_client, dedicated_endpoint
15
+ from auditqa.utils import make_html_source, parse_output_llm_with_sources, save_logs, get_message_template, get_client_location, get_client_ip
16
  from dotenv import load_dotenv
17
  from threading import Lock
18
+ # import json
19
+ # from functools import partial
20
+ # import time
21
+ from gradio.routes import Request
22
 
23
  # TESTING DEBUG LOG
24
  from auditqa.logging_config import setup_logging
25
  setup_logging()
26
  import logging
27
  logger = logging.getLogger(__name__)
28
+ logger.setLevel(logging.DEBUG) # Ensure debug logging is enabled
29
 
30
  load_dotenv()
31
 
 
102
  # Still need to return the expected outputs even on error
103
  return gr.update(visible=False), gr.update(visible=True)
104
 
105
+ # Session Manager added (track session duration & location)
106
+ class SessionManager:
107
+ def __init__(self):
108
+ self.sessions = {}
109
+
110
+ def create_session(self, client_ip):
111
+ session_id = str(uuid4())
112
+ self.sessions[session_id] = {
113
+ 'start_time': datetime.now(),
114
+ 'last_activity': datetime.now(),
115
+ 'client_ip': client_ip,
116
+ 'location_info': get_client_location(client_ip)
117
+ }
118
+ return session_id
119
+
120
+ def update_session(self, session_id):
121
+ if session_id in self.sessions:
122
+ self.sessions[session_id]['last_activity'] = datetime.now()
123
+
124
+ def get_session_duration(self, session_id):
125
+ if session_id in self.sessions:
126
+ start = self.sessions[session_id]['start_time']
127
+ last = self.sessions[session_id]['last_activity']
128
+ return (last - start).total_seconds()
129
+ return 0
130
+
131
+ def get_session_data(self, session_id):
132
+ return self.sessions.get(session_id)
133
+
134
+ # Initialize session manager
135
+ session_manager = SessionManager()
136
+
137
+ async def chat(query, history, sources, reports, subtype, year, client_ip=None, session_id=None):
138
+ """Update chat function to handle session data"""
139
+ # TESTING: DEBUG LOG
140
+ logger.debug(f"Chat function called with query: {query}")
141
+ logger.debug(f"Client IP: {client_ip}")
142
+ logger.debug(f"Session ID: {session_id}")
143
+
144
+ if not session_id: # Session managment
145
+ session_id = session_manager.create_session(client_ip)
146
+ logger.debug(f"Created new session: {session_id}")
147
+ else:
148
+ session_manager.update_session(session_id)
149
+ logger.debug(f"Updated existing session: {session_id}")
150
+
151
+ # Get session data
152
+ session_data = session_manager.get_session_data(session_id)
153
+ session_duration = session_manager.get_session_duration(session_id)
154
+ logger.debug(f"Session duration: {session_duration}")
155
+
156
  print(f">> NEW QUESTION : {query}")
157
  print(f"history:{history}")
158
  print(f"sources:{sources}")
 
207
 
208
  ##-----------------------get answer from endpoints------------------------------
209
  answer_yet = ""
210
+ # Logs strcuture updated for feedback + session data (moved up here because: feedback)
211
  timestamp = str(datetime.now().timestamp())
212
  logs_data = {
213
+ "session_id": session_id,
214
+ "client_location": session_data['location_info'],
215
+ "session_duration_seconds": session_duration,
216
  "system_prompt": SYSTEM_PROMPT,
217
  "sources": sources,
218
  "reports": reports,
 
223
  "endpoint_type": model_config.get('reader','TYPE'),
224
  "reader": model_config.get('reader','NVIDIA_MODEL'),
225
  "docs": [doc.page_content for doc in context_retrieved],
226
+ "answer": "",
227
  "time": timestamp,
228
  }
229
 
 
248
  history[-1] = (query, parsed_answer)
249
  # Update logs_data with current answer
250
  logs_data["answer"] = parsed_answer
251
+ yield [tuple(x) for x in history], docs_html, logs_data, session_id
252
 
253
  # Stream the response updates
254
  async for update in process_stream():
255
  yield update
256
 
257
  else:
258
+ chat_model = dedicated_endpoint() # TESTING: ADAPTED FOR HF INFERENCE API (needs to be reverted for production version)
259
  async def process_stream():
260
  nonlocal answer_yet
261
  try:
 
280
  history[-1] = (query, parsed_answer)
281
  # Update logs_data with current answer
282
  logs_data["answer"] = parsed_answer
283
+ yield [tuple(x) for x in history], docs_html, logs_data, session_id
284
  await asyncio.sleep(0.05)
285
 
286
  except Exception as e:
 
534
 
535
  #-------------------- Gradio voodoo continued -------------------------
536
 
537
+ # Add these state components at the top level of the Blocks
538
+ session_id = gr.State(None)
539
+ client_ip = gr.State(None)
540
+
541
+ @demo.load(api_name="get_client_ip")
542
+ def get_client_ip_handler(dummy_input="", request: gr.Request = None):
543
+ """Handler for getting client IP in Gradio context"""
544
+ return get_client_ip(request)
545
+
546
+ # Update the event handlers
547
  (textbox
548
+ .submit(get_client_ip_handler, [textbox], [client_ip], api_name="get_ip_textbox")
549
+ .then(start_chat, [textbox, chatbot], [textbox, tabs, chatbot], queue=False, api_name="start_chat_textbox")
550
+ .then(chat,
551
+ [textbox, chatbot, dropdown_sources, dropdown_reports, dropdown_category, dropdown_year, client_ip, session_id],
552
+ [chatbot, sources_textbox, feedback_state, session_id],
553
+ queue=True, concurrency_limit=8, api_name="chat_textbox")
554
  .then(show_feedback, [feedback_state], [feedback_row, feedback_thanks, feedback_state], api_name="show_feedback_textbox")
555
  .then(finish_chat, None, [textbox], api_name="finish_chat_textbox"))
556
 
557
  (examples_hidden
558
  .change(start_chat, [examples_hidden, chatbot], [textbox, tabs, chatbot], queue=False, api_name="start_chat_examples")
559
+ .then(get_client_ip_handler, [examples_hidden], [client_ip], api_name="get_ip_examples")
560
+ .then(chat,
561
+ [examples_hidden, chatbot, dropdown_sources, dropdown_reports, dropdown_category, dropdown_year, client_ip, session_id],
562
+ [chatbot, sources_textbox, feedback_state, session_id],
563
+ concurrency_limit=8, api_name="chat_examples")
564
  .then(show_feedback, [feedback_state], [feedback_row, feedback_thanks, feedback_state], api_name="show_feedback_examples")
565
  .then(finish_chat, None, [textbox], api_name="finish_chat_examples"))
566
 
 
 
567
  demo.queue()
568
 
569
  demo.launch()
auditqa/__pycache__/__init__.cpython-310.pyc DELETED
Binary file (141 Bytes)
 
auditqa/__pycache__/process_chunks.cpython-310.pyc DELETED
Binary file (5.06 kB)
 
auditqa/__pycache__/reader.cpython-310.pyc DELETED
Binary file (1.86 kB)
 
auditqa/__pycache__/reports.cpython-310.pyc DELETED
Binary file (13.3 kB)
 
auditqa/__pycache__/retriever.cpython-310.pyc DELETED
Binary file (2.05 kB)
 
auditqa/__pycache__/sample_questions.cpython-310.pyc DELETED
Binary file (4.91 kB)
 
auditqa/__pycache__/utils.cpython-310.pyc DELETED
Binary file (2.81 kB)
 
auditqa/utils.py CHANGED
@@ -5,6 +5,12 @@ from langchain.schema import (
5
  HumanMessage,
6
  SystemMessage,
7
  )
 
 
 
 
 
 
8
 
9
  def save_logs(scheduler, JSON_DATASET_PATH, logs, feedback=None) -> None:
10
  """ Every interaction with app saves the log of question and answer,
@@ -73,4 +79,55 @@ def parse_output_llm_with_sources(output):
73
  else:
74
  parts.append(part)
75
  content_parts = "".join(parts)
76
- return content_parts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  HumanMessage,
6
  SystemMessage,
7
  )
8
+ import requests
9
+ from datetime import datetime
10
+ from uuid import uuid4
11
+
12
+
13
+
14
 
15
  def save_logs(scheduler, JSON_DATASET_PATH, logs, feedback=None) -> None:
16
  """ Every interaction with app saves the log of question and answer,
 
79
  else:
80
  parts.append(part)
81
  content_parts = "".join(parts)
82
+ return content_parts
83
+
84
+ def get_client_ip(request=None):
85
+ """Get the client IP address from the request context"""
86
+ try:
87
+ if request:
88
+ # Try different headers that might contain the real IP
89
+ ip = request.client.host
90
+ # Check for proxy headers
91
+ forwarded_for = request.headers.get('X-Forwarded-For')
92
+ if forwarded_for:
93
+ # X-Forwarded-For can contain multiple IPs - first one is the client
94
+ ip = forwarded_for.split(',')[0].strip()
95
+
96
+ logging.debug(f"Client IP detected: {ip}")
97
+ return ip
98
+ except Exception as e:
99
+ logging.error(f"Error getting client IP: {e}")
100
+ return "127.0.0.1"
101
+
102
+
103
+ def get_client_location(ip_address) -> dict | None:
104
+ """Get geolocation info using ipapi.co"""
105
+ # Add headers so we don't get blocked...
106
+ headers = {
107
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
108
+ }
109
+ try:
110
+ response = requests.get(
111
+ f'https://ipapi.co/{ip_address}/json/',
112
+ headers=headers,
113
+ timeout=5
114
+ )
115
+ if response.status_code == 200:
116
+ data = response.json()
117
+ return {
118
+ 'city': data.get('city'),
119
+ 'region': data.get('region'),
120
+ 'country': data.get('country_name'),
121
+ 'latitude': data.get('latitude'),
122
+ 'longitude': data.get('longitude')
123
+ }
124
+ elif response.status_code == 429:
125
+ logging.warning(f"Rate limit exceeded. Response: {response.text}")
126
+ return None
127
+ else:
128
+ logging.error(f"Error: Status code {response.status_code}. Response: {response.text}")
129
+ return None
130
+
131
+ except requests.exceptions.RequestException as e:
132
+ logging.error(f"Request failed: {str(e)}")
133
+ return None