kd13 commited on
Commit
7e2fea5
·
verified ·
1 Parent(s): 1cfd5ed

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +208 -0
  2. requirements.txt +13 -0
app.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import logging
3
+ import os
4
+ import threading
5
+ from dataclasses import dataclass, field
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ from lexquery.config import GEMINI_MODEL, LANGUAGES
10
+ from lexquery.models import LocalModels
11
+ from lexquery.pipeline import Engine
12
+ from lexquery.schema import Result
13
+ import gradio as gr
14
+
15
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s %(name)s: %(message)s')
16
+
17
+
18
+ @lru_cache(maxsize=1)
19
+ def load_models():
20
+ return LocalModels()
21
+
22
+
23
+ @dataclass
24
+ class Session:
25
+ engine: Engine = field(default_factory=lambda: Engine(load_models()))
26
+ chat: list = field(default_factory=list)
27
+ lock: object = field(default_factory=threading.RLock)
28
+
29
+
30
+ _sessions = {}
31
+ _sessions_lock = threading.RLock()
32
+
33
+
34
+ def session(request):
35
+ if not request.session_hash:
36
+ raise gr.Error('Open the app in a browser session first.')
37
+ with _sessions_lock:
38
+ if request.session_hash not in _sessions:
39
+ _sessions[request.session_hash] = Session()
40
+ return _sessions[request.session_hash]
41
+
42
+
43
+ def cleanup_id(session_id):
44
+ with _sessions_lock:
45
+ _sessions.pop(session_id, None)
46
+
47
+
48
+ def unload(request: gr.Request):
49
+ cleanup_id(request.session_hash)
50
+
51
+
52
+ def initialize(request: gr.Request):
53
+ session(request)
54
+ return request.session_hash
55
+
56
+
57
+ def safe_error(exc):
58
+ text = str(exc)
59
+ for name in ('GOOGLE_API_KEY', 'GEMINI_API_KEY'):
60
+ key = os.getenv(name)
61
+ if key:
62
+ text = text.replace(key, '[key hidden]')
63
+ return text
64
+
65
+
66
+ def read_uploads(files, request: gr.Request, progress=gr.Progress()):
67
+ item = session(request)
68
+ with item.lock:
69
+ keep, messages = set(), []
70
+ old_ids = set(item.engine.documents)
71
+ for i, file in enumerate(files or []):
72
+ path = Path(file)
73
+ progress(i / max(1, len(files)), desc='Reading ' + path.name)
74
+ try:
75
+ doc = item.engine.ingest(path.name, path.read_bytes())
76
+ keep.add(doc.id)
77
+ messages.append(f'**{html.escape(doc.name)}** · {len(doc.pages)} pages · ready')
78
+ uncertain = [str(p.number) for p in doc.pages if p.warnings]
79
+ if uncertain:
80
+ messages.append('Extraction notes on pages ' + ', '.join(uncertain) + '; see Extracted text.')
81
+ except Exception as exc:
82
+ logging.exception('PDF extraction failed')
83
+ messages.append(f'**{html.escape(path.name)}** · {html.escape(safe_error(exc))}')
84
+ for doc_id in set(item.engine.documents) - keep:
85
+ del item.engine.documents[doc_id]
86
+ if old_ids != keep:
87
+ item.engine.invalidate()
88
+ item.engine.history.clear()
89
+ item.chat.clear()
90
+ choices = [(d.name, d.id) for d in item.engine.documents.values()]
91
+ ids = [i for _, i in choices]
92
+ progress(1, desc='Finished')
93
+ return ('\n\n'.join(messages) or 'Upload a PDF to begin.',
94
+ gr.Dropdown(choices=choices, value=ids, multiselect=True),
95
+ gr.Dropdown(choices=choices, value=ids[0] if ids else None),
96
+ [], '', '')
97
+
98
+
99
+ def extracted_text(doc_id, request: gr.Request):
100
+ item = session(request)
101
+ with item.lock:
102
+ doc = item.engine.documents.get(doc_id)
103
+ if not doc:
104
+ return ''
105
+ return '\n\n'.join(f'PAGE {p.number} · {p.language}\n{p.original}\n' +
106
+ ('\nNotes: ' + '; '.join(p.warnings) if p.warnings else '') for p in doc.pages)
107
+
108
+
109
+ def result_markdown(result):
110
+ paragraphs = [html.escape(text) for text in result.displayed] if result.status in (
111
+ 'checked', 'conversational') else []
112
+ for note in result.notes:
113
+ paragraphs.append(html.escape(safe_error(note)))
114
+ return '\n\n'.join(paragraphs) or 'I could not find a supported answer in the selected documents.'
115
+
116
+
117
+ def ask(question, ids, language, request: gr.Request):
118
+ item = session(request)
119
+ with item.lock:
120
+ if not question.strip():
121
+ return item.chat, ''
122
+ result = item.engine.present(item.engine.ask(question, ids or []), language)
123
+ item.chat.extend([{'role': 'user', 'content': question},
124
+ {'role': 'assistant', 'content': result_markdown(result)}])
125
+ return item.chat, ''
126
+
127
+
128
+ def summarize(ids, language, request: gr.Request, progress=gr.Progress()):
129
+ item = session(request)
130
+ with item.lock:
131
+ if not ids:
132
+ return 'Upload and select a PDF first.'
133
+ sections = []
134
+ for i, doc_id in enumerate(ids):
135
+ doc = item.engine.documents.get(doc_id)
136
+ if doc is None:
137
+ continue
138
+ progress(i / len(ids), desc='Summarizing ' + doc.name)
139
+ try:
140
+ report = item.engine.report(doc_id)
141
+ result = item.engine.present(Result.model_validate(report['summary']), language)
142
+ section = '### ' + html.escape(doc.name) + '\n\n' + result_markdown(result)
143
+ fields = []
144
+ for name, facts in report['fields'].items():
145
+ if facts:
146
+ fields.append('**' + name.replace('_', ' ').title() + ':** ' +
147
+ '; '.join(html.escape(f['value']) for f in facts))
148
+ if fields:
149
+ section += '\n\n' + '\n\n'.join(fields)
150
+ for note in report.get('field_notes', []):
151
+ section += '\n\n' + html.escape(safe_error(note))
152
+ sections.append(section)
153
+ except Exception as exc:
154
+ logging.exception('Report failed')
155
+ sections.append('### ' + html.escape(doc.name) + '\n\n' + html.escape(safe_error(exc)))
156
+ progress(1, desc='Finished')
157
+ return '\n\n---\n\n'.join(sections)
158
+
159
+
160
+ def clear_chat(request: gr.Request):
161
+ item = session(request)
162
+ with item.lock:
163
+ item.engine.history.clear()
164
+ item.chat.clear()
165
+ return []
166
+
167
+
168
+ def build_app():
169
+ with gr.Blocks(title='LexQuery', delete_cache=(3600, 3600)) as demo:
170
+ lifecycle = gr.State(None, delete_callback=cleanup_id)
171
+ gr.Markdown('# LexQuery\nUpload your PDFs. Ask questions. Get clear summaries.')
172
+ with gr.Row():
173
+ with gr.Column(scale=1, min_width=280):
174
+ files = gr.File(label='Documents', file_types=['.pdf'], file_count='multiple', type='filepath')
175
+ gr.Markdown('Scanned and text PDFs · English and 22 Indian languages')
176
+ status = gr.Markdown('Upload a PDF to begin.')
177
+ selected = gr.Dropdown(choices=[], multiselect=True, label='Use these documents')
178
+ language = gr.Dropdown(LANGUAGES, value='English', label='Answer language')
179
+ with gr.Column(scale=3):
180
+ with gr.Tab('Questions'):
181
+ chat = gr.Chatbot(label='Conversation', height=460)
182
+ question = gr.Textbox(label='Your question', placeholder='What does this document say about…?')
183
+ with gr.Row():
184
+ ask_button = gr.Button('Ask', variant='primary')
185
+ reset = gr.Button('New conversation')
186
+ with gr.Tab('Summaries'):
187
+ gr.Markdown('Summaries cover each selected document independently of your conversation.')
188
+ summary_button = gr.Button('Summarize documents', variant='primary')
189
+ summary = gr.Markdown()
190
+ with gr.Accordion('Extracted text', open=False):
191
+ text_doc = gr.Dropdown(choices=[], label='Document')
192
+ transcript = gr.Textbox(label='Original text', lines=18, interactive=False)
193
+ gr.Markdown('Check important findings against the original PDF.')
194
+ files.change(read_uploads, files, [status, selected, text_doc, chat, summary, transcript])
195
+ text_doc.change(extracted_text, text_doc, transcript)
196
+ for trigger in (ask_button.click, question.submit):
197
+ trigger(ask, [question, selected, language], [chat, question])
198
+ reset.click(clear_chat, outputs=chat)
199
+ summary_button.click(summarize, [selected, language], summary)
200
+ demo.load(initialize, outputs=lifecycle)
201
+ demo.unload(unload)
202
+ return demo.queue(default_concurrency_limit=1)
203
+
204
+
205
+ if __name__ == '__main__':
206
+ logging.info('LexQuery Gemini model: %s', GEMINI_MODEL)
207
+ build_app().launch(server_name=os.getenv('GRADIO_SERVER_NAME', '127.0.0.1'),
208
+ server_port=int(os.getenv('GRADIO_SERVER_PORT', '7860')), share=False)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.17.3
2
+ langchain>=1,<2
3
+ langchain-google-genai>=4.4,<5
4
+ google-genai>=2.20,<3
5
+ langgraph>=1,<2
6
+ python-dotenv>=1,<2
7
+ llama-cpp-python>=0.3.16,<0.4
8
+ sentence-transformers>=3.4,<6
9
+ transformers>=4.48,<5
10
+ huggingface-hub>=0.34,<1
11
+ numpy==1.26.4
12
+ rank-bm25==0.2.2
13
+ pydantic>=2.9,<3