browser-assistant 2.8.5 → 2.8.6
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.
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/server/main.py +19 -3
- package/server/vision_images.py +91 -3
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
package/server/main.py
CHANGED
|
@@ -24,7 +24,8 @@ from fastapi.exception_handlers import http_exception_handler
|
|
|
24
24
|
from local_security import install_local_guards, redact, sanitize_data
|
|
25
25
|
from vision_images import (compress_image_base64, unique_image_blocks, collect_image_evidence,
|
|
26
26
|
cross_check_visual_answers, relative_percentage_question, RELATIVE_PERCENTAGE_INSTRUCTION,
|
|
27
|
-
answer_permutation, cross_check_text_answers,
|
|
27
|
+
answer_permutation, cross_check_text_answers, solve_matrix_choice,
|
|
28
|
+
VisionRateLimitError, VisionUnconfirmedError)
|
|
28
29
|
|
|
29
30
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
30
31
|
logger = logging.getLogger("browser-assistant")
|
|
@@ -190,7 +191,7 @@ async def lifespan(_app: FastAPI):
|
|
|
190
191
|
client.close()
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
app = FastAPI(title="Browser Assistant API", version="2.8.
|
|
194
|
+
app = FastAPI(title="Browser Assistant API", version="2.8.6", lifespan=lifespan)
|
|
194
195
|
install_local_guards(app)
|
|
195
196
|
|
|
196
197
|
@app.exception_handler(HTTPException)
|
|
@@ -1190,6 +1191,13 @@ async def review_and_correct_code(
|
|
|
1190
1191
|
|
|
1191
1192
|
# ── Routes ────────────────────────────────────────────────────────────────────
|
|
1192
1193
|
|
|
1194
|
+
def calculated_matrix_result(text, language):
|
|
1195
|
+
item = solve_matrix_choice(text)
|
|
1196
|
+
if item is not None:
|
|
1197
|
+
return {"results": json.dumps([item], ensure_ascii=False), "provider": "local matrix calculation [verified]",
|
|
1198
|
+
"language": language, "answer_verified": True, "code_reviewed": False}
|
|
1199
|
+
|
|
1200
|
+
|
|
1193
1201
|
@app.post("/solve")
|
|
1194
1202
|
async def solve_questions(content: PageContent):
|
|
1195
1203
|
# Trim oversized page text to stay within token limits
|
|
@@ -1232,6 +1240,10 @@ REPAIR MODE:
|
|
|
1232
1240
|
)
|
|
1233
1241
|
strict_code_review = require_code and (content.review_before_submit or not FAST_CODE_MODE or STRICT_REPAIR_REVIEW)
|
|
1234
1242
|
verify_answers = not require_code and (content.verify_answer or not FAST_SCAN_MODE)
|
|
1243
|
+
if not require_code and content.practice_type in {'auto', 'mcq', 'math', 'aptitude'}:
|
|
1244
|
+
matrix_result = calculated_matrix_result(trimmed_text, language)
|
|
1245
|
+
if matrix_result is not None:
|
|
1246
|
+
return matrix_result
|
|
1235
1247
|
|
|
1236
1248
|
async def confirm_answers(cleaned, source_model=None):
|
|
1237
1249
|
if not verify_answers or not any(item['type'] == 'mcq' for item in json.loads(cleaned)):
|
|
@@ -1395,6 +1407,11 @@ async def _solve_vision(content: VisionContent):
|
|
|
1395
1407
|
compressed.append(await asyncio.to_thread(compress_image_base64, image, max_dim=2400, quality=95))
|
|
1396
1408
|
except ValueError as exc:
|
|
1397
1409
|
raise HTTPException(422, f'Image {index}: {exc}') from exc
|
|
1410
|
+
is_code_scan = content.practice_type == "code" or (content.practice_type == "auto" and bool(content.language))
|
|
1411
|
+
if not is_code_scan and content.practice_type in {'auto', 'mcq', 'math', 'aptitude'}:
|
|
1412
|
+
matrix_result = calculated_matrix_result(content.text, content.language or CODE_DEFAULT_LANGUAGE)
|
|
1413
|
+
if matrix_result is not None:
|
|
1414
|
+
return matrix_result
|
|
1398
1415
|
image_blocks, image_positions = unique_image_blocks(compressed)
|
|
1399
1416
|
|
|
1400
1417
|
clients = tuple(_groq_clients)
|
|
@@ -1406,7 +1423,6 @@ async def _solve_vision(content: VisionContent):
|
|
|
1406
1423
|
loop = asyncio.get_running_loop()
|
|
1407
1424
|
errors = []
|
|
1408
1425
|
rate_limit_resets = []
|
|
1409
|
-
is_code_scan = content.practice_type == "code" or (content.practice_type == "auto" and bool(content.language))
|
|
1410
1426
|
comparison = None if is_code_scan else relative_percentage_question(content.text)
|
|
1411
1427
|
unconfirmed = False
|
|
1412
1428
|
vision_token_limit = 4096 if is_code_scan else VISION_MAX_TOKENS
|
package/server/vision_images.py
CHANGED
|
@@ -170,9 +170,13 @@ def answer_permutation(value):
|
|
|
170
170
|
MATRIX_SYNTAX = re.compile(r'\\begin\{(?:[bpBvV]?matrix|smallmatrix)\}')
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def
|
|
173
|
+
def unwrap_math(value):
|
|
174
174
|
text = re.sub(r'^\${1,2}|\${1,2}$', '', value.strip())
|
|
175
|
-
|
|
175
|
+
return re.sub(r'^\\[\[(]|\\[\])]$', '', text).strip().replace('−', '-')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def matrix_cells(value):
|
|
179
|
+
text = unwrap_math(value)
|
|
176
180
|
latex = re.fullmatch(r'\\begin\{([bpBvV]?matrix|smallmatrix)\}(.*?)\\end\{\1\}', text, re.S)
|
|
177
181
|
array = re.fullmatch(r'\[\s*(\[[^\[\]]+\](?:\s*,\s*\[[^\[\]]+\])*)\s*\]', text)
|
|
178
182
|
if latex:
|
|
@@ -181,13 +185,97 @@ def numeric_matrix_key(value):
|
|
|
181
185
|
rows = [row.split(',') for row in re.findall(r'\[([^\[\]]+)\]', array[1])]
|
|
182
186
|
else:
|
|
183
187
|
return None
|
|
184
|
-
|
|
188
|
+
return [[cell.strip() for cell in row] for row in rows]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def numeric_matrix_key(value):
|
|
192
|
+
cells = matrix_cells(value)
|
|
185
193
|
if cells and all(len(row) == len(cells[0]) and all(
|
|
186
194
|
re.fullmatch(r'[+-]?(?:\d+(?:\.\d+)?|\.\d+)', cell, re.ASCII) for cell in row) for row in cells):
|
|
187
195
|
return json.dumps(cells, separators=(',', ':'))
|
|
188
196
|
return None
|
|
189
197
|
|
|
190
198
|
|
|
199
|
+
def matrix_2x2(value):
|
|
200
|
+
rows = matrix_cells(value)
|
|
201
|
+
if not rows or len(rows) != 2 or any(len(row) != 2 for row in rows):
|
|
202
|
+
return None
|
|
203
|
+
entries = []
|
|
204
|
+
for cell in (cell for row in rows for cell in row):
|
|
205
|
+
cell = re.sub(r'^([+-]?)\\(?:[dt]?frac)\{([+-]?\d{1,15})\}\{(\d{1,15})\}$',
|
|
206
|
+
r'\1\2/\3', cell)
|
|
207
|
+
if not re.fullmatch(rf'(?:{DECIMAL_VALUE}|[+-]?\d{{1,15}}/\d{{1,15}})', cell):
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
entries.append(Fraction(cell))
|
|
211
|
+
except ZeroDivisionError:
|
|
212
|
+
return None
|
|
213
|
+
return tuple(entries)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def solve_matrix_choice(text):
|
|
217
|
+
"""Calculate a self-contained matrix question with one matching displayed choice."""
|
|
218
|
+
question, separator, option_text = text.strip().rpartition('\nA.')
|
|
219
|
+
if not separator:
|
|
220
|
+
return None
|
|
221
|
+
question = re.sub(r'\n(?:Question format: )?Select one answer\.$', '', question, flags=re.I)
|
|
222
|
+
question = re.sub(r'^\d+\s*of\s*\d+\s*questions?\s*\d+\s*marks?\s*', '', question, flags=re.I)
|
|
223
|
+
# Math delimiters and layout whitespace do not carry meaning in these prompts.
|
|
224
|
+
prompt = re.sub(r'\s+', ' ', question.replace('$', '')).strip()
|
|
225
|
+
inverse = re.fullmatch(
|
|
226
|
+
r'For ([A-Z])\s*=\s*(.+?),\s*what is \1\^(?:\{-1\}|-1)'
|
|
227
|
+
r'(?: computed using the adjugate method)?\?', prompt,
|
|
228
|
+
)
|
|
229
|
+
product = re.fullmatch(
|
|
230
|
+
r'For matrices ([A-Z]) with shape \(\s*([a-z])\s*,\s*([a-z])\s*\) and '
|
|
231
|
+
r'([A-Z]) with shape \(\s*([a-z])\s*,\s*([a-z])\s*\),\s*'
|
|
232
|
+
r'which condition must hold for the product (\1\4|\4\1) to be defined\?', prompt,
|
|
233
|
+
)
|
|
234
|
+
if inverse:
|
|
235
|
+
entries = matrix_2x2(inverse[2])
|
|
236
|
+
if entries is None:
|
|
237
|
+
return None
|
|
238
|
+
a, b, c, d = entries
|
|
239
|
+
determinant = a * d - b * c
|
|
240
|
+
if not determinant:
|
|
241
|
+
return None
|
|
242
|
+
expected = (d / determinant, -b / determinant, -c / determinant, a / determinant)
|
|
243
|
+
parse_option = matrix_2x2
|
|
244
|
+
reasoning = f'The determinant is ({a})({d}) − ({b})({c}) = {determinant}. Divide the adjugate by the determinant.'
|
|
245
|
+
elif product:
|
|
246
|
+
first, first_rows, first_cols, second, second_rows, second_cols, order = product.groups()
|
|
247
|
+
if first == second or len({first_rows, first_cols, second_rows, second_cols}) != 4:
|
|
248
|
+
return None
|
|
249
|
+
left_cols, right_rows = (first_cols, second_rows) if order == first + second else (second_cols, first_rows)
|
|
250
|
+
expected = frozenset((left_cols, right_rows))
|
|
251
|
+
|
|
252
|
+
def parse_option(value):
|
|
253
|
+
equation = re.fullmatch(r'([a-z])\s*=\s*([a-z])', unwrap_math(value))
|
|
254
|
+
return frozenset(equation.groups()) if equation else None
|
|
255
|
+
|
|
256
|
+
reasoning = f'The left matrix has {left_cols} columns and the right matrix has {right_rows} rows. These must be equal.'
|
|
257
|
+
else:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
lines = ('A.' + option_text).splitlines()
|
|
261
|
+
if not 2 <= len(lines) <= 26:
|
|
262
|
+
return None
|
|
263
|
+
matches = []
|
|
264
|
+
for index, line in enumerate(lines):
|
|
265
|
+
letter = chr(ord('A') + index)
|
|
266
|
+
option = re.fullmatch(rf'{letter}\.\s*(\S.*)', line)
|
|
267
|
+
if not option:
|
|
268
|
+
return None
|
|
269
|
+
value = parse_option(option[1])
|
|
270
|
+
if value is None:
|
|
271
|
+
return None
|
|
272
|
+
if value == expected:
|
|
273
|
+
matches.append(f'Option {letter}: {option[1]}')
|
|
274
|
+
if len(matches) != 1:
|
|
275
|
+
return None
|
|
276
|
+
return {'type': 'mcq', 'question': question, 'answer': matches[0], 'reasoning': reasoning}
|
|
277
|
+
|
|
278
|
+
|
|
191
279
|
def normalize_checked_answer(value, *, matrix_math=False):
|
|
192
280
|
if answer_permutation(value):
|
|
193
281
|
return json.dumps(value)
|