refactor: convert f-string logging to lazy %-style format (G004)
Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.
Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.
Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
(printf-style specs aren't 1:1 with Python format specs, so we
delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved
Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a4e6125d28
commit
665cb9b1eb
@@ -91,7 +91,9 @@ class DocumentChunker:
|
||||
]
|
||||
|
||||
logger.debug(
|
||||
f"Chunked document into {len(chunks)} chunks "
|
||||
f"(chunk_size={self.chunk_size}, overlap={self.overlap})"
|
||||
"Chunked document into %s chunks (chunk_size=%s, overlap=%s)",
|
||||
len(chunks),
|
||||
self.chunk_size,
|
||||
self.overlap,
|
||||
)
|
||||
return chunks
|
||||
|
||||
@@ -42,7 +42,7 @@ def html_to_markdown(html_content: str | None) -> str:
|
||||
)
|
||||
return markdown.strip()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to convert HTML to Markdown: {e}")
|
||||
logger.warning("Failed to convert HTML to Markdown: %s", e)
|
||||
# Fallback: strip all HTML tags as a last resort
|
||||
|
||||
text = re.sub(r"<[^>]+>", " ", html_content)
|
||||
|
||||
@@ -104,7 +104,7 @@ async def get_user_client_basic_auth(
|
||||
f"User must configure background sync in Astrolabe personal settings."
|
||||
)
|
||||
|
||||
logger.info(f"Using app password for background sync: {user_id}")
|
||||
logger.info("Using app password for background sync: %s", user_id)
|
||||
return NextcloudClient(
|
||||
base_url=nextcloud_host,
|
||||
username=user_id,
|
||||
@@ -141,7 +141,7 @@ async def get_user_client_oauth(
|
||||
f"User must complete the OAuth provisioning flow."
|
||||
)
|
||||
|
||||
logger.info(f"Using OAuth refresh token for background sync: {user_id}")
|
||||
logger.info("Using OAuth refresh token for background sync: %s", user_id)
|
||||
return NextcloudClient.from_token(
|
||||
base_url=nextcloud_host,
|
||||
token=token,
|
||||
@@ -208,7 +208,7 @@ async def user_scanner_task(
|
||||
task_status: Status object for signaling task readiness
|
||||
"""
|
||||
mode_label = "BasicAuth" if use_basic_auth else "OAuth"
|
||||
logger.info(f"[{mode_label}] Scanner started for user: {user_id}")
|
||||
logger.info("[%s] Scanner started for user: %s", mode_label, user_id)
|
||||
settings = get_settings()
|
||||
max_consecutive_errors = 5
|
||||
|
||||
@@ -221,12 +221,14 @@ async def user_scanner_task(
|
||||
)
|
||||
try:
|
||||
await nc_client.capabilities() # Lightweight OCS call to validate creds
|
||||
logger.info(f"[{mode_label}] Credentials validated for {user_id}")
|
||||
logger.info("[%s] Credentials validated for %s", mode_label, user_id)
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code in (401, 403):
|
||||
logger.warning(
|
||||
f"[{mode_label}] Credential validation failed for {user_id} "
|
||||
f"(HTTP {e.response.status_code}), not starting scan loop"
|
||||
"[%s] Credential validation failed for %s (HTTP %s), not starting scan loop",
|
||||
mode_label,
|
||||
user_id,
|
||||
e.response.status_code,
|
||||
)
|
||||
return
|
||||
raise
|
||||
@@ -234,13 +236,15 @@ async def user_scanner_task(
|
||||
await nc_client.close()
|
||||
except NotProvisionedError:
|
||||
logger.warning(
|
||||
f"[{mode_label}] User {user_id} not provisioned, not starting scan loop"
|
||||
"[%s] User %s not provisioned, not starting scan loop", mode_label, user_id
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[{mode_label}] Pre-validation failed for {user_id}: {e}. "
|
||||
f"Proceeding to scan loop (has its own error handling)."
|
||||
"[%s] Pre-validation failed for %s: %s. Proceeding to scan loop (has its own error handling).",
|
||||
mode_label,
|
||||
user_id,
|
||||
e,
|
||||
)
|
||||
|
||||
consecutive_errors = 0
|
||||
@@ -264,7 +268,9 @@ async def user_scanner_task(
|
||||
|
||||
except NotProvisionedError:
|
||||
logger.warning(
|
||||
f"[{mode_label}] User {user_id} no longer provisioned, stopping scanner"
|
||||
"[%s] User %s no longer provisioned, stopping scanner",
|
||||
mode_label,
|
||||
user_id,
|
||||
)
|
||||
break
|
||||
|
||||
@@ -272,16 +278,19 @@ async def user_scanner_task(
|
||||
status_code = e.response.status_code
|
||||
if status_code in (401, 403):
|
||||
logger.warning(
|
||||
f"[{mode_label}] Scanner auth failed for {user_id} "
|
||||
f"(HTTP {status_code}), stopping scanner. "
|
||||
f"User may need to re-provision credentials."
|
||||
"[%s] Scanner auth failed for %s (HTTP %s), stopping scanner. User may need to re-provision credentials.",
|
||||
mode_label,
|
||||
user_id,
|
||||
status_code,
|
||||
)
|
||||
break
|
||||
elif status_code == 429:
|
||||
retry_after = min(int(e.response.headers.get("Retry-After", "60")), 300)
|
||||
logger.warning(
|
||||
f"[{mode_label}] Scanner rate-limited for {user_id}, "
|
||||
f"backing off {retry_after}s"
|
||||
"[%s] Scanner rate-limited for %s, backing off %ss",
|
||||
mode_label,
|
||||
user_id,
|
||||
retry_after,
|
||||
)
|
||||
try:
|
||||
with anyio.move_on_after(retry_after):
|
||||
@@ -294,16 +303,24 @@ async def user_scanner_task(
|
||||
else:
|
||||
consecutive_errors += 1
|
||||
logger.error(
|
||||
f"[{mode_label}] Scanner HTTP error for {user_id}: {e} "
|
||||
f"({consecutive_errors}/{max_consecutive_errors})",
|
||||
"[%s] Scanner HTTP error for %s: %s (%s/%s)",
|
||||
mode_label,
|
||||
user_id,
|
||||
e,
|
||||
consecutive_errors,
|
||||
max_consecutive_errors,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
logger.error(
|
||||
f"[{mode_label}] Scanner error for {user_id}: {e} "
|
||||
f"({consecutive_errors}/{max_consecutive_errors})",
|
||||
"[%s] Scanner error for %s: %s (%s/%s)",
|
||||
mode_label,
|
||||
user_id,
|
||||
e,
|
||||
consecutive_errors,
|
||||
max_consecutive_errors,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -313,8 +330,10 @@ async def user_scanner_task(
|
||||
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
logger.error(
|
||||
f"[{mode_label}] Scanner for {user_id} hit {max_consecutive_errors} "
|
||||
f"consecutive errors, stopping scanner"
|
||||
"[%s] Scanner for %s hit %s consecutive errors, stopping scanner",
|
||||
mode_label,
|
||||
user_id,
|
||||
max_consecutive_errors,
|
||||
)
|
||||
break
|
||||
|
||||
@@ -325,7 +344,7 @@ async def user_scanner_task(
|
||||
except anyio.get_cancelled_exc_class():
|
||||
break
|
||||
|
||||
logger.info(f"[{mode_label}] Scanner stopped for user: {user_id}")
|
||||
logger.info("[%s] Scanner stopped for user: %s", mode_label, user_id)
|
||||
|
||||
|
||||
async def multi_user_processor_task(
|
||||
@@ -352,7 +371,7 @@ async def multi_user_processor_task(
|
||||
task_status: Status object for signaling task readiness
|
||||
"""
|
||||
mode_label = "BasicAuth" if use_basic_auth else "OAuth"
|
||||
logger.info(f"[{mode_label}] Processor {worker_id} started")
|
||||
logger.info("[%s] Processor %s started", mode_label, worker_id)
|
||||
task_status.started()
|
||||
|
||||
while not shutdown_event.is_set():
|
||||
@@ -378,34 +397,47 @@ async def multi_user_processor_task(
|
||||
continue
|
||||
|
||||
except anyio.EndOfStream:
|
||||
logger.info(f"[{mode_label}] Processor {worker_id}: Stream closed, exiting")
|
||||
logger.info(
|
||||
"[%s] Processor %s: Stream closed, exiting", mode_label, worker_id
|
||||
)
|
||||
break
|
||||
|
||||
except NotProvisionedError:
|
||||
if doc_task:
|
||||
logger.warning(
|
||||
f"[{mode_label}] User {doc_task.user_id} not provisioned, "
|
||||
f"skipping {doc_task.doc_type}_{doc_task.doc_id}"
|
||||
"[%s] User %s not provisioned, skipping %s_%s",
|
||||
mode_label,
|
||||
doc_task.user_id,
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
if doc_task:
|
||||
logger.error(
|
||||
f"[{mode_label}] Processor {worker_id} error processing "
|
||||
f"{doc_task.doc_type}_{doc_task.doc_id}: {e}",
|
||||
"[%s] Processor %s error processing %s_%s: %s",
|
||||
mode_label,
|
||||
worker_id,
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"[{mode_label}] Processor {worker_id} error: {e}", exc_info=True
|
||||
"[%s] Processor %s error: %s",
|
||||
mode_label,
|
||||
worker_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
finally:
|
||||
if nc_client:
|
||||
await nc_client.close()
|
||||
|
||||
logger.info(f"[{mode_label}] Processor {worker_id} stopped")
|
||||
logger.info("[%s] Processor %s stopped", mode_label, worker_id)
|
||||
|
||||
|
||||
# Backward compatibility alias
|
||||
@@ -482,7 +514,7 @@ async def user_manager_task(
|
||||
mode_label = "BasicAuth" if use_basic_auth else "OAuth"
|
||||
|
||||
logger.info(
|
||||
f"[{mode_label}] User manager started (poll interval: {poll_interval}s)"
|
||||
"[%s] User manager started (poll interval: %ss)", mode_label, poll_interval
|
||||
)
|
||||
task_status.started()
|
||||
|
||||
@@ -503,7 +535,9 @@ async def user_manager_task(
|
||||
new_users = provisioned_users - active_users
|
||||
for user_id in new_users:
|
||||
logger.info(
|
||||
f"[{mode_label}] Starting scanner for newly provisioned user: {user_id}"
|
||||
"[%s] Starting scanner for newly provisioned user: %s",
|
||||
mode_label,
|
||||
user_id,
|
||||
)
|
||||
cancel_scope = anyio.CancelScope()
|
||||
user_states[user_id] = UserSyncState(
|
||||
@@ -529,7 +563,7 @@ async def user_manager_task(
|
||||
revoked_users = active_users - provisioned_users
|
||||
for user_id in revoked_users:
|
||||
logger.info(
|
||||
f"[{mode_label}] Stopping scanner for revoked user: {user_id}"
|
||||
"[%s] Stopping scanner for revoked user: %s", mode_label, user_id
|
||||
)
|
||||
state = user_states.get(user_id)
|
||||
if state:
|
||||
@@ -537,12 +571,16 @@ async def user_manager_task(
|
||||
# Note: state will be removed by _run_user_scanner_with_scope on exit
|
||||
|
||||
if new_users:
|
||||
logger.info(f"[{mode_label}] Started {len(new_users)} new scanner(s)")
|
||||
logger.info(
|
||||
"[%s] Started %s new scanner(s)", mode_label, len(new_users)
|
||||
)
|
||||
if revoked_users:
|
||||
logger.info(f"[{mode_label}] Stopped {len(revoked_users)} scanner(s)")
|
||||
logger.info(
|
||||
"[%s] Stopped %s scanner(s)", mode_label, len(revoked_users)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{mode_label}] User manager error: {e}", exc_info=True)
|
||||
logger.error("[%s] User manager error: %s", mode_label, e, exc_info=True)
|
||||
|
||||
# Sleep until next poll
|
||||
try:
|
||||
@@ -553,9 +591,11 @@ async def user_manager_task(
|
||||
|
||||
# Cancel all remaining scanners on shutdown
|
||||
logger.info(
|
||||
f"[{mode_label}] User manager shutting down, cancelling {len(user_states)} scanner(s)"
|
||||
"[%s] User manager shutting down, cancelling %s scanner(s)",
|
||||
mode_label,
|
||||
len(user_states),
|
||||
)
|
||||
for state in list(user_states.values()):
|
||||
state.cancel_scope.cancel()
|
||||
|
||||
logger.info(f"[{mode_label}] User manager stopped")
|
||||
logger.info("[%s] User manager stopped", mode_label)
|
||||
|
||||
@@ -92,9 +92,11 @@ class PCA:
|
||||
self.explained_variance_ratio_ = np.zeros(self.n_components)
|
||||
|
||||
logger.debug(
|
||||
f"PCA fit: {n_samples} samples, {n_features} features → "
|
||||
f"{self.n_components} components, "
|
||||
f"explained variance: {self.explained_variance_ratio_}"
|
||||
"PCA fit: %s samples, %s features → %s components, explained variance: %s",
|
||||
n_samples,
|
||||
n_features,
|
||||
self.n_components,
|
||||
self.explained_variance_ratio_,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@@ -122,13 +122,19 @@ async def write_placeholder_point(
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Wrote placeholder for {doc_type}_{doc_id} (user={user_id}, "
|
||||
f"modified_at={modified_at})"
|
||||
"Wrote placeholder for %s_%s (user=%s, modified_at=%s)",
|
||||
doc_type,
|
||||
doc_id,
|
||||
user_id,
|
||||
modified_at,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to write placeholder for {doc_type}_{doc_id}: {e}",
|
||||
"Failed to write placeholder for %s_%s: %s",
|
||||
doc_type,
|
||||
doc_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
@@ -180,7 +186,9 @@ async def query_document_metadata(
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error querying document metadata for {doc_type}_{doc_id}: {e}")
|
||||
logger.warning(
|
||||
"Error querying document metadata for %s_%s: %s", doc_type, doc_id, e
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -219,11 +227,16 @@ async def delete_placeholder_point(
|
||||
),
|
||||
)
|
||||
|
||||
logger.debug(f"Deleted placeholder for {doc_type}_{doc_id} (user={user_id})")
|
||||
logger.debug(
|
||||
"Deleted placeholder for %s_%s (user=%s)", doc_type, doc_id, user_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete placeholder for {doc_type}_{doc_id}: {e}",
|
||||
"Failed to delete placeholder for %s_%s: %s",
|
||||
doc_type,
|
||||
doc_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
@@ -271,13 +284,16 @@ async def update_placeholder_status(
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Updated placeholder status for {doc_type}_{doc_id} to '{status}' "
|
||||
f"(user={user_id})"
|
||||
"Updated placeholder status for %s_%s to '%s' (user=%s)",
|
||||
doc_type,
|
||||
doc_id,
|
||||
status,
|
||||
user_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to update placeholder status for {doc_type}_{doc_id}: {e}"
|
||||
"Failed to update placeholder status for %s_%s: %s", doc_type, doc_id, e
|
||||
)
|
||||
# Don't raise - status updates are non-critical
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ async def processor_task(
|
||||
user_id: User being processed
|
||||
task_status: Status object for signaling task readiness
|
||||
"""
|
||||
logger.info(f"Processor {worker_id} started")
|
||||
logger.info("Processor %s started", worker_id)
|
||||
|
||||
# Signal that the task has started and is ready
|
||||
task_status.started()
|
||||
@@ -130,18 +130,21 @@ async def processor_task(
|
||||
|
||||
except anyio.EndOfStream:
|
||||
# Scanner finished and closed stream, exit gracefully
|
||||
logger.info(f"Processor {worker_id}: Scanner finished, exiting")
|
||||
logger.info("Processor %s: Scanner finished, exiting", worker_id)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Processor {worker_id} error processing "
|
||||
f"{doc_task.doc_type}_{doc_task.doc_id}: {e}",
|
||||
"Processor %s error processing %s_%s: %s",
|
||||
worker_id,
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
# Continue to next document (no task_done() needed with streams)
|
||||
|
||||
logger.info(f"Processor {worker_id} stopped")
|
||||
logger.info("Processor %s stopped", worker_id)
|
||||
|
||||
|
||||
async def process_document(doc_task: DocumentTask, nc_client: NextcloudClient):
|
||||
@@ -157,8 +160,11 @@ async def process_document(doc_task: DocumentTask, nc_client: NextcloudClient):
|
||||
start_time = time.time()
|
||||
|
||||
logger.debug(
|
||||
f"Processing {doc_task.doc_type}_{doc_task.doc_id} "
|
||||
f"for {doc_task.user_id} ({doc_task.operation})"
|
||||
"Processing %s_%s for %s (%s)",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
doc_task.user_id,
|
||||
doc_task.operation,
|
||||
)
|
||||
|
||||
with trace_operation(
|
||||
@@ -197,7 +203,10 @@ async def process_document(doc_task: DocumentTask, nc_client: NextcloudClient):
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
f"Deleted {doc_task.doc_type}_{doc_task.doc_id} for {doc_task.user_id}"
|
||||
"Deleted %s_%s for %s",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
doc_task.user_id,
|
||||
)
|
||||
|
||||
# Record successful deletion metrics
|
||||
@@ -223,15 +232,22 @@ async def process_document(doc_task: DocumentTask, nc_client: NextcloudClient):
|
||||
except (HTTPStatusError, Exception) as e:
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(
|
||||
f"Retry {attempt + 1}/{max_retries} for "
|
||||
f"{doc_task.doc_type}_{doc_task.doc_id}: {e}"
|
||||
"Retry %s/%s for %s_%s: %s",
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
e,
|
||||
)
|
||||
await anyio.sleep(retry_delay)
|
||||
retry_delay *= 2 # Exponential backoff
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to index {doc_task.doc_type}_{doc_task.doc_id} "
|
||||
f"after {max_retries} retries: {e}"
|
||||
"Failed to index %s_%s after %s retries: %s",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
max_retries,
|
||||
e,
|
||||
)
|
||||
# Record failed processing metrics
|
||||
duration = time.time() - start_time
|
||||
@@ -348,7 +364,11 @@ async def _index_document(
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to fetch card with metadata (board_id={board_id}, stack_id={stack_id}, card_id={doc_task.doc_id}): {e}, falling back to iteration"
|
||||
"Failed to fetch card with metadata (board_id=%s, stack_id=%s, card_id=%s): %s, falling back to iteration",
|
||||
board_id,
|
||||
stack_id,
|
||||
doc_task.doc_id,
|
||||
e,
|
||||
)
|
||||
|
||||
# Fallback: Iterate through all boards/stacks (for legacy data or if fast path failed)
|
||||
@@ -454,27 +474,32 @@ async def _index_document(
|
||||
if "page_boundaries" in file_metadata:
|
||||
page_boundaries = file_metadata["page_boundaries"]
|
||||
logger.info(
|
||||
f"Page boundaries for {file_path}: "
|
||||
f"{len(page_boundaries)} pages, text length: {len(content)}"
|
||||
"Page boundaries for %s: %s pages, text length: %s",
|
||||
file_path,
|
||||
len(page_boundaries),
|
||||
len(content),
|
||||
)
|
||||
# Log first 3 page boundaries for debugging
|
||||
for boundary in page_boundaries[:3]:
|
||||
logger.debug(
|
||||
f" Page {boundary['page']}: "
|
||||
f"offsets [{boundary['start_offset']}:{boundary['end_offset']}]"
|
||||
" Page %s: offsets [%s:%s]",
|
||||
boundary["page"],
|
||||
boundary["start_offset"],
|
||||
boundary["end_offset"],
|
||||
)
|
||||
# Verify last boundary matches text length
|
||||
if page_boundaries:
|
||||
last_boundary = page_boundaries[-1]
|
||||
if last_boundary["end_offset"] != len(content):
|
||||
logger.warning(
|
||||
f"Text length mismatch: content={len(content)}, "
|
||||
f"last_boundary_end={last_boundary['end_offset']}"
|
||||
"Text length mismatch: content=%s, last_boundary_end=%s",
|
||||
len(content),
|
||||
last_boundary["end_offset"],
|
||||
)
|
||||
else:
|
||||
logger.debug(f"No page_boundaries in metadata for {file_path}")
|
||||
logger.debug("No page_boundaries in metadata for %s", file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process file {file_path}: {e}")
|
||||
logger.error("Failed to process file %s: %s", file_path, e)
|
||||
raise
|
||||
|
||||
# Tokenize and chunk (using configured chunk size and overlap)
|
||||
@@ -509,26 +534,32 @@ async def _index_document(
|
||||
# Diagnostic: Verify page number assignment
|
||||
assigned_count = sum(1 for c in chunks if c.page_number is not None)
|
||||
logger.info(
|
||||
f"Assigned page numbers to {assigned_count}/{len(chunks)} chunks "
|
||||
f"for {file_path}"
|
||||
"Assigned page numbers to %s/%s chunks for %s",
|
||||
assigned_count,
|
||||
len(chunks),
|
||||
file_path,
|
||||
)
|
||||
|
||||
# Log first 3 chunks to see their page assignments
|
||||
for i, chunk in enumerate(chunks[:3]):
|
||||
logger.debug(
|
||||
f" Chunk {i}: page={chunk.page_number}, "
|
||||
f"offsets=[{chunk.start_offset}:{chunk.end_offset}]"
|
||||
" Chunk %s: page=%s, offsets=[%s:%s]",
|
||||
i,
|
||||
chunk.page_number,
|
||||
chunk.start_offset,
|
||||
chunk.end_offset,
|
||||
)
|
||||
|
||||
# Warning if NO page numbers were assigned
|
||||
if assigned_count == 0:
|
||||
logger.warning(
|
||||
f"NO page numbers assigned! "
|
||||
f"Text length: {len(content)}, "
|
||||
f"Chunks: {len(chunks)}, "
|
||||
f"Chunk offset range: [{chunks[0].start_offset}:{chunks[-1].end_offset}], "
|
||||
f"Page boundaries: {len(page_boundaries_list)} pages, "
|
||||
f"First boundary: {page_boundaries_list[0] if page_boundaries_list else 'None'}"
|
||||
"NO page numbers assigned! Text length: %s, Chunks: %s, Chunk offset range: [%s:%s], Page boundaries: %s pages, First boundary: %s",
|
||||
len(content),
|
||||
len(chunks),
|
||||
chunks[0].start_offset,
|
||||
chunks[-1].end_offset,
|
||||
len(page_boundaries_list),
|
||||
page_boundaries_list[0] if page_boundaries_list else "None",
|
||||
)
|
||||
|
||||
# Extract chunk texts for embedding
|
||||
@@ -603,7 +634,7 @@ async def _index_document(
|
||||
|
||||
page_boundaries_list = cast(list[dict[str, Any]], page_boundaries)
|
||||
|
||||
logger.info(f"Computing chunk bboxes for {len(chunk_data)} PDF chunks")
|
||||
logger.info("Computing chunk bboxes for %s PDF chunks", len(chunk_data))
|
||||
|
||||
batch_results = await anyio.to_thread.run_sync( # type: ignore[attr-defined]
|
||||
lambda: PDFHighlighter.compute_chunk_bboxes_batch(
|
||||
@@ -617,7 +648,9 @@ async def _index_document(
|
||||
for chunk_index, (bboxes, _) in batch_results.items():
|
||||
chunk_bboxes[chunk_index] = bboxes
|
||||
|
||||
logger.info(f"Computed bboxes for {len(chunk_bboxes)}/{len(chunks)} chunks")
|
||||
logger.info(
|
||||
"Computed bboxes for %s/%s chunks", len(chunk_bboxes), len(chunks)
|
||||
)
|
||||
|
||||
# Run all embedding/highlighting operations in parallel
|
||||
# - Dense embeddings: I/O bound (API call)
|
||||
@@ -753,7 +786,10 @@ async def _index_document(
|
||||
except Exception as e:
|
||||
# Log but don't fail indexing if placeholder deletion fails
|
||||
logger.warning(
|
||||
f"Failed to delete placeholder for {doc_task.doc_type}_{doc_task.doc_id}: {e}"
|
||||
"Failed to delete placeholder for %s_%s: %s",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
e,
|
||||
)
|
||||
|
||||
# Upsert to Qdrant in batches. Now that we no longer embed PNG payloads,
|
||||
@@ -779,10 +815,15 @@ async def _index_document(
|
||||
)
|
||||
if batch_end < len(points):
|
||||
logger.debug(
|
||||
f"Upserted batch {batch_start // BATCH_SIZE + 1}/{(len(points) + BATCH_SIZE - 1) // BATCH_SIZE}"
|
||||
"Upserted batch %s/%s",
|
||||
batch_start // BATCH_SIZE + 1,
|
||||
(len(points) + BATCH_SIZE - 1) // BATCH_SIZE,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Indexed {doc_task.doc_type}_{doc_task.doc_id} for {doc_task.user_id} "
|
||||
f"({len(chunks)} chunks)"
|
||||
"Indexed %s_%s for %s (%s chunks)",
|
||||
doc_task.doc_type,
|
||||
doc_task.doc_id,
|
||||
doc_task.user_id,
|
||||
len(chunks),
|
||||
)
|
||||
|
||||
@@ -534,7 +534,7 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
# Detect mode and initialize client accordingly
|
||||
if settings.qdrant_url:
|
||||
# Network mode
|
||||
logger.info(f"Using Qdrant network mode: {settings.qdrant_url}")
|
||||
logger.info("Using Qdrant network mode: %s", settings.qdrant_url)
|
||||
provisional = AsyncQdrantClient(
|
||||
url=settings.qdrant_url,
|
||||
api_key=settings.qdrant_api_key,
|
||||
@@ -548,7 +548,7 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
else:
|
||||
# Persistent local mode - use path parameter
|
||||
logger.info(
|
||||
f"Using Qdrant persistent mode: {settings.qdrant_location}"
|
||||
"Using Qdrant persistent mode: %s", settings.qdrant_location
|
||||
)
|
||||
provisional = AsyncQdrantClient(path=settings.qdrant_location)
|
||||
else:
|
||||
@@ -580,14 +580,14 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
# `get_collection()`) is the only existence-probe permitted on a
|
||||
# collection-scoped JWT — it returns 200 with the collection
|
||||
# detail on hit and 404 on miss.
|
||||
logger.debug(f"Fetching collection '{collection_name}' details...")
|
||||
logger.debug("Fetching collection '%s' details...", collection_name)
|
||||
collection_info = None
|
||||
try:
|
||||
collection_info = await provisional.get_collection(collection_name)
|
||||
except UnexpectedResponse as exc:
|
||||
if exc.status_code != 404:
|
||||
raise
|
||||
logger.debug(f"Collection '{collection_name}' not found (404).")
|
||||
logger.debug("Collection '%s' not found (404).", collection_name)
|
||||
except ValueError as exc:
|
||||
# Local/in-memory qdrant_client raises ValueError(f"Collection
|
||||
# {name} not found") instead of UnexpectedResponse — see
|
||||
@@ -602,12 +602,12 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
# multi-user-basic CI jobs all exercise this path.
|
||||
if "not found" not in str(exc):
|
||||
raise
|
||||
logger.debug(f"Collection '{collection_name}' not found (local mode).")
|
||||
logger.debug("Collection '%s' not found (local mode).", collection_name)
|
||||
|
||||
if collection_info is not None:
|
||||
# Collection exists - validate dimensions
|
||||
logger.debug(
|
||||
f"Collection '{collection_name}' found, validating dimensions..."
|
||||
"Collection '%s' found, validating dimensions...", collection_name
|
||||
)
|
||||
# Handle both named vectors (dict) and legacy single vector
|
||||
vectors = collection_info.config.params.vectors
|
||||
@@ -633,8 +633,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Using existing Qdrant collection: {collection_name} "
|
||||
f"(dimension={actual_dimension}, model={settings.get_embedding_model_name()})"
|
||||
"Using existing Qdrant collection: %s (dimension=%s, model=%s)",
|
||||
collection_name,
|
||||
actual_dimension,
|
||||
settings.get_embedding_model_name(),
|
||||
)
|
||||
|
||||
# Existing collections may pre-date the doc_id normalization /
|
||||
@@ -658,8 +660,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
# Collection doesn't exist - create it
|
||||
embedding_model = settings.get_embedding_model_name()
|
||||
logger.info(
|
||||
f"Collection '{collection_name}' not found, creating with "
|
||||
f"dimension={expected_dimension}, model={embedding_model}..."
|
||||
"Collection '%s' not found, creating with dimension=%s, model=%s...",
|
||||
collection_name,
|
||||
expected_dimension,
|
||||
embedding_model,
|
||||
)
|
||||
await provisional.create_collection(
|
||||
collection_name=collection_name,
|
||||
@@ -678,12 +682,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
f"Created Qdrant collection: {collection_name}\n"
|
||||
f" Dense vector dimension: {expected_dimension}\n"
|
||||
f" Dense embedding model: {embedding_model}\n"
|
||||
f" Sparse vectors: BM25 (for hybrid search)\n"
|
||||
f" Distance: COSINE\n"
|
||||
f"Background sync will index all documents with dense + sparse vectors."
|
||||
"Created Qdrant collection: %s\\n Dense vector dimension: %s\\n Dense embedding model: %s\\n Sparse vectors: BM25 (for hybrid search)\\n Distance: COSINE\\nBackground sync will index all documents with dense + sparse vectors.",
|
||||
collection_name,
|
||||
expected_dimension,
|
||||
embedding_model,
|
||||
)
|
||||
# Freshly created collection has no payload schema yet; pass
|
||||
# {} explicitly to skip the otherwise-redundant
|
||||
|
||||
@@ -155,7 +155,7 @@ async def get_last_indexed_timestamp(user_id: str) -> int | None:
|
||||
)
|
||||
|
||||
num_points = len(points)
|
||||
logger.info(f"Found {num_points} indexed notes in Qdrant for user {user_id}")
|
||||
logger.info("Found %s indexed notes in Qdrant for user %s", num_points, user_id)
|
||||
|
||||
if points:
|
||||
timestamps = [
|
||||
@@ -165,14 +165,16 @@ async def get_last_indexed_timestamp(user_id: str) -> int | None:
|
||||
]
|
||||
max_timestamp = max(timestamps) if timestamps else 0
|
||||
logger.info(
|
||||
f"Max indexed_at: {max_timestamp}, timestamps sample: {timestamps[:3]}"
|
||||
"Max indexed_at: %s, timestamps sample: %s",
|
||||
max_timestamp,
|
||||
timestamps[:3],
|
||||
)
|
||||
return int(max_timestamp) if max_timestamp > 0 else None
|
||||
|
||||
logger.info(f"No indexed notes found for user {user_id}")
|
||||
logger.info("No indexed notes found for user %s", user_id)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get last indexed timestamp: {e}", exc_info=True)
|
||||
logger.warning("Failed to get last indexed timestamp: %s", e, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -198,7 +200,7 @@ async def scanner_task(
|
||||
user_id: User to scan
|
||||
task_status: Status object for signaling task readiness
|
||||
"""
|
||||
logger.info(f"Scanner task started for user: {user_id}")
|
||||
logger.info("Scanner task started for user: %s", user_id)
|
||||
settings = get_settings()
|
||||
|
||||
# Signal that the task has started and is ready
|
||||
@@ -215,7 +217,7 @@ async def scanner_task(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Scanner error: {e}", exc_info=True)
|
||||
logger.error("Scanner error: %s", e, exc_info=True)
|
||||
|
||||
# Sleep until next interval or wake event
|
||||
try:
|
||||
@@ -247,7 +249,10 @@ async def scan_user_documents(
|
||||
|
||||
scan_id = random.randint(1000, 9999)
|
||||
logger.info(
|
||||
f"[SCAN-{scan_id}] Starting scan for user: {user_id}, initial_sync={initial_sync}"
|
||||
"[SCAN-%s] Starting scan for user: %s, initial_sync=%s",
|
||||
scan_id,
|
||||
user_id,
|
||||
initial_sync,
|
||||
)
|
||||
|
||||
with trace_operation(
|
||||
@@ -266,7 +271,9 @@ async def scan_user_documents(
|
||||
)
|
||||
if prune_before:
|
||||
logger.info(
|
||||
f"[SCAN-{scan_id}] Using pruneBefore={prune_before} to optimize data transfer"
|
||||
"[SCAN-%s] Using pruneBefore=%s to optimize data transfer",
|
||||
scan_id,
|
||||
prune_before,
|
||||
)
|
||||
|
||||
# For deletion tracking, get all doc_ids in Qdrant (for incremental sync)
|
||||
@@ -303,7 +310,7 @@ async def scan_user_documents(
|
||||
if point.payload is not None and "doc_id" in point.payload
|
||||
}
|
||||
|
||||
logger.debug(f"Found {len(indexed_doc_ids)} indexed documents in Qdrant")
|
||||
logger.debug("Found %s indexed documents in Qdrant", len(indexed_doc_ids))
|
||||
|
||||
# Stream notes from Nextcloud and process immediately
|
||||
note_count = 0
|
||||
@@ -341,7 +348,8 @@ async def scan_user_documents(
|
||||
doc_key = (user_id, doc_id)
|
||||
if doc_key in _potentially_deleted:
|
||||
logger.debug(
|
||||
f"Document {doc_id} reappeared, removing from deletion grace period"
|
||||
"Document %s reappeared, removing from deletion grace period",
|
||||
doc_id,
|
||||
)
|
||||
del _potentially_deleted[doc_key]
|
||||
|
||||
@@ -368,14 +376,17 @@ async def scan_user_documents(
|
||||
stale_threshold = get_settings().vector_sync_scan_interval * 5
|
||||
if placeholder_age > stale_threshold:
|
||||
logger.debug(
|
||||
f"Found stale placeholder for note {doc_id} "
|
||||
f"(age={placeholder_age:.1f}s), requeuing"
|
||||
"Found stale placeholder for note %s (age=%ss), requeuing",
|
||||
doc_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
)
|
||||
needs_indexing = True
|
||||
else:
|
||||
logger.debug(
|
||||
f"Skipping note {doc_id} with recent placeholder "
|
||||
f"(age={placeholder_age:.1f}s < {stale_threshold:.1f}s)"
|
||||
"Skipping note %s with recent placeholder (age=%ss < %ss)",
|
||||
doc_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
format(stale_threshold, ".1f"),
|
||||
)
|
||||
|
||||
if needs_indexing:
|
||||
@@ -399,11 +410,11 @@ async def scan_user_documents(
|
||||
queued += 1
|
||||
|
||||
# Log and record metrics after streaming
|
||||
logger.info(f"[SCAN-{scan_id}] Found {note_count} notes for {user_id}")
|
||||
logger.info("[SCAN-%s] Found %s notes for %s", scan_id, note_count, user_id)
|
||||
record_vector_sync_scan(note_count)
|
||||
|
||||
if initial_sync:
|
||||
logger.info(f"Sent {queued} documents for initial sync: {user_id}")
|
||||
logger.info("Sent %s documents for initial sync: %s", queued, user_id)
|
||||
return
|
||||
|
||||
# Check for deleted documents (in Qdrant but not in Nextcloud)
|
||||
@@ -426,8 +437,10 @@ async def scan_user_documents(
|
||||
if time_missing >= grace_period:
|
||||
# Grace period elapsed, send for deletion
|
||||
logger.info(
|
||||
f"Document {doc_id} missing for {time_missing:.1f}s "
|
||||
f"(>{grace_period:.1f}s grace period), sending deletion"
|
||||
"Document %s missing for %ss (>%ss grace period), sending deletion",
|
||||
doc_id,
|
||||
format(time_missing, ".1f"),
|
||||
format(grace_period, ".1f"),
|
||||
)
|
||||
await send_stream.send(
|
||||
DocumentTask(
|
||||
@@ -443,13 +456,16 @@ async def scan_user_documents(
|
||||
del _potentially_deleted[doc_key]
|
||||
else:
|
||||
logger.debug(
|
||||
f"Document {doc_id} still missing "
|
||||
f"({time_missing:.1f}s/{grace_period:.1f}s grace period)"
|
||||
"Document %s still missing (%ss/%ss grace period)",
|
||||
doc_id,
|
||||
format(time_missing, ".1f"),
|
||||
format(grace_period, ".1f"),
|
||||
)
|
||||
else:
|
||||
# First time missing, add to grace period tracking
|
||||
logger.debug(
|
||||
f"Document {doc_id} missing for first time, starting grace period"
|
||||
"Document %s missing for first time, starting grace period",
|
||||
doc_id,
|
||||
)
|
||||
_potentially_deleted[doc_key] = current_time
|
||||
|
||||
@@ -476,7 +492,7 @@ async def scan_user_documents(
|
||||
if point.payload is not None and "doc_id" in point.payload
|
||||
}
|
||||
|
||||
logger.debug(f"Found {len(indexed_file_ids)} indexed files in Qdrant")
|
||||
logger.debug("Found %s indexed files in Qdrant", len(indexed_file_ids))
|
||||
|
||||
# Scan for tagged PDF files
|
||||
file_count = 0
|
||||
@@ -568,7 +584,9 @@ async def scan_user_documents(
|
||||
file_key = (user_id, file_id)
|
||||
if file_key in _potentially_deleted:
|
||||
logger.debug(
|
||||
f"File {file_path} (ID: {file_id}) reappeared, removing from deletion grace period"
|
||||
"File %s (ID: %s) reappeared, removing from deletion grace period",
|
||||
file_path,
|
||||
file_id,
|
||||
)
|
||||
del _potentially_deleted[file_key]
|
||||
|
||||
@@ -595,14 +613,19 @@ async def scan_user_documents(
|
||||
stale_threshold = get_settings().vector_sync_scan_interval * 5
|
||||
if placeholder_age > stale_threshold:
|
||||
logger.debug(
|
||||
f"Found stale placeholder for file {file_path} (ID: {file_id}) "
|
||||
f"(age={placeholder_age:.1f}s), requeuing"
|
||||
"Found stale placeholder for file %s (ID: %s) (age=%ss), requeuing",
|
||||
file_path,
|
||||
file_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
)
|
||||
needs_indexing = True
|
||||
else:
|
||||
logger.debug(
|
||||
f"Skipping file {file_path} (ID: {file_id}) with recent placeholder "
|
||||
f"(age={placeholder_age:.1f}s < {stale_threshold:.1f}s)"
|
||||
"Skipping file %s (ID: %s) with recent placeholder (age=%ss < %ss)",
|
||||
file_path,
|
||||
file_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
format(stale_threshold, ".1f"),
|
||||
)
|
||||
|
||||
if needs_indexing:
|
||||
@@ -627,7 +650,7 @@ async def scan_user_documents(
|
||||
file_queued += 1
|
||||
|
||||
logger.info(
|
||||
f"[SCAN-{scan_id}] Found {file_count} tagged PDFs for {user_id}"
|
||||
"[SCAN-%s] Found %s tagged PDFs for %s", scan_id, file_count, user_id
|
||||
)
|
||||
record_vector_sync_scan(file_count)
|
||||
|
||||
@@ -645,8 +668,10 @@ async def scan_user_documents(
|
||||
if time_missing >= grace_period:
|
||||
# Grace period elapsed, send for deletion
|
||||
logger.info(
|
||||
f"File ID {file_id} missing for {time_missing:.1f}s "
|
||||
f"(>{grace_period:.1f}s grace period), sending deletion"
|
||||
"File ID %s missing for %ss (>%ss grace period), sending deletion",
|
||||
file_id,
|
||||
format(time_missing, ".1f"),
|
||||
format(grace_period, ".1f"),
|
||||
)
|
||||
await send_stream.send(
|
||||
DocumentTask(
|
||||
@@ -662,12 +687,13 @@ async def scan_user_documents(
|
||||
else:
|
||||
# First time missing, add to grace period tracking
|
||||
logger.debug(
|
||||
f"File ID {file_id} missing for first time, starting grace period"
|
||||
"File ID %s missing for first time, starting grace period",
|
||||
file_id,
|
||||
)
|
||||
_potentially_deleted[file_key] = current_time
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to scan tagged files for {user_id}: {e}")
|
||||
logger.warning("Failed to scan tagged files for %s: %s", user_id, e)
|
||||
|
||||
queued += file_queued
|
||||
|
||||
@@ -683,7 +709,7 @@ async def scan_user_documents(
|
||||
)
|
||||
queued += news_queued
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to scan news items for {user_id}: {e}")
|
||||
logger.warning("Failed to scan news items for %s: %s", user_id, e)
|
||||
|
||||
# Scan Deck cards
|
||||
deck_queued = 0
|
||||
@@ -697,14 +723,19 @@ async def scan_user_documents(
|
||||
)
|
||||
queued += deck_queued
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to scan deck cards for {user_id}: {e}")
|
||||
logger.warning("Failed to scan deck cards for %s: %s", user_id, e)
|
||||
|
||||
if queued > 0:
|
||||
logger.info(
|
||||
f"Sent {queued} documents ({file_queued} files, {news_queued} news items, {deck_queued} deck cards) for incremental sync: {user_id}"
|
||||
"Sent %s documents (%s files, %s news items, %s deck cards) for incremental sync: %s",
|
||||
queued,
|
||||
file_queued,
|
||||
news_queued,
|
||||
deck_queued,
|
||||
user_id,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"No changes detected for {user_id}")
|
||||
logger.debug("No changes detected for %s", user_id)
|
||||
|
||||
|
||||
async def scan_news_items(
|
||||
@@ -754,7 +785,7 @@ async def scan_news_items(
|
||||
for point in points
|
||||
if point.payload is not None and "doc_id" in point.payload
|
||||
}
|
||||
logger.debug(f"Found {len(indexed_item_ids)} indexed news items in Qdrant")
|
||||
logger.debug("Found %s indexed news items in Qdrant", len(indexed_item_ids))
|
||||
|
||||
# Fetch all items (News app caps at ~200 per feed via auto-purge)
|
||||
all_items = await nc_client.news.get_items(
|
||||
@@ -762,7 +793,7 @@ async def scan_news_items(
|
||||
type_=NewsItemType.ALL,
|
||||
get_read=True,
|
||||
)
|
||||
logger.debug(f"[SCAN-{scan_id}] Found {len(all_items)} news items")
|
||||
logger.debug("[SCAN-%s] Found %s news items", scan_id, len(all_items))
|
||||
|
||||
item_count = len(all_items)
|
||||
nextcloud_item_ids: set[str] = set()
|
||||
@@ -800,7 +831,8 @@ async def scan_news_items(
|
||||
doc_key = (user_id, doc_id)
|
||||
if doc_key in _potentially_deleted:
|
||||
logger.debug(
|
||||
f"News item {doc_id} reappeared, removing from deletion grace period"
|
||||
"News item %s reappeared, removing from deletion grace period",
|
||||
doc_id,
|
||||
)
|
||||
del _potentially_deleted[doc_key]
|
||||
|
||||
@@ -820,8 +852,9 @@ async def scan_news_items(
|
||||
stale_threshold = settings.vector_sync_scan_interval * 5
|
||||
if placeholder_age > stale_threshold:
|
||||
logger.debug(
|
||||
f"Found stale placeholder for news item {doc_id} "
|
||||
f"(age={placeholder_age:.1f}s), requeuing"
|
||||
"Found stale placeholder for news item %s (age=%ss), requeuing",
|
||||
doc_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
)
|
||||
needs_indexing = True
|
||||
|
||||
@@ -844,7 +877,10 @@ async def scan_news_items(
|
||||
queued += 1
|
||||
|
||||
logger.info(
|
||||
f"[SCAN-{scan_id}] Found {item_count} news items (starred+unread) for {user_id}"
|
||||
"[SCAN-%s] Found %s news items (starred+unread) for %s",
|
||||
scan_id,
|
||||
item_count,
|
||||
user_id,
|
||||
)
|
||||
record_vector_sync_scan(item_count)
|
||||
|
||||
@@ -864,8 +900,10 @@ async def scan_news_items(
|
||||
|
||||
if time_missing >= grace_period:
|
||||
logger.info(
|
||||
f"News item {doc_id} missing for {time_missing:.1f}s "
|
||||
f"(>{grace_period:.1f}s grace period), sending deletion"
|
||||
"News item %s missing for %ss (>%ss grace period), sending deletion",
|
||||
doc_id,
|
||||
format(time_missing, ".1f"),
|
||||
format(grace_period, ".1f"),
|
||||
)
|
||||
await send_stream.send(
|
||||
DocumentTask(
|
||||
@@ -880,7 +918,8 @@ async def scan_news_items(
|
||||
del _potentially_deleted[doc_key]
|
||||
else:
|
||||
logger.debug(
|
||||
f"News item {doc_id} missing for first time, starting grace period"
|
||||
"News item %s missing for first time, starting grace period",
|
||||
doc_id,
|
||||
)
|
||||
_potentially_deleted[doc_key] = current_time
|
||||
|
||||
@@ -932,11 +971,11 @@ async def scan_deck_cards(
|
||||
for point in points
|
||||
if point.payload is not None and "doc_id" in point.payload
|
||||
}
|
||||
logger.debug(f"Found {len(indexed_card_ids)} indexed deck cards in Qdrant")
|
||||
logger.debug("Found %s indexed deck cards in Qdrant", len(indexed_card_ids))
|
||||
|
||||
# Fetch all boards
|
||||
boards = await nc_client.deck.get_boards()
|
||||
logger.debug(f"[SCAN-{scan_id}] Found {len(boards)} deck boards")
|
||||
logger.debug("[SCAN-%s] Found %s deck boards", scan_id, len(boards))
|
||||
|
||||
card_count = 0
|
||||
nextcloud_card_ids: set[str] = set()
|
||||
@@ -949,7 +988,7 @@ async def scan_deck_cards(
|
||||
|
||||
# Skip deleted boards (soft delete: deletedAt > 0)
|
||||
if board.deletedAt > 0:
|
||||
logger.debug(f"[SCAN-{scan_id}] Skipping deleted board {board.id}")
|
||||
logger.debug("[SCAN-%s] Skipping deleted board %s", scan_id, board.id)
|
||||
continue
|
||||
|
||||
# Get stacks for this board
|
||||
@@ -998,7 +1037,8 @@ async def scan_deck_cards(
|
||||
doc_key = (user_id, doc_id)
|
||||
if doc_key in _potentially_deleted:
|
||||
logger.debug(
|
||||
f"Deck card {doc_id} reappeared, removing from deletion grace period"
|
||||
"Deck card %s reappeared, removing from deletion grace period",
|
||||
doc_id,
|
||||
)
|
||||
del _potentially_deleted[doc_key]
|
||||
|
||||
@@ -1018,8 +1058,9 @@ async def scan_deck_cards(
|
||||
stale_threshold = settings.vector_sync_scan_interval * 5
|
||||
if placeholder_age > stale_threshold:
|
||||
logger.debug(
|
||||
f"Found stale placeholder for deck card {doc_id} "
|
||||
f"(age={placeholder_age:.1f}s), requeuing"
|
||||
"Found stale placeholder for deck card %s (age=%ss), requeuing",
|
||||
doc_id,
|
||||
format(placeholder_age, ".1f"),
|
||||
)
|
||||
needs_indexing = True
|
||||
|
||||
@@ -1043,7 +1084,10 @@ async def scan_deck_cards(
|
||||
queued += 1
|
||||
|
||||
logger.info(
|
||||
f"[SCAN-{scan_id}] Found {card_count} deck cards (non-archived) for {user_id}"
|
||||
"[SCAN-%s] Found %s deck cards (non-archived) for %s",
|
||||
scan_id,
|
||||
card_count,
|
||||
user_id,
|
||||
)
|
||||
record_vector_sync_scan(card_count)
|
||||
|
||||
@@ -1062,8 +1106,10 @@ async def scan_deck_cards(
|
||||
|
||||
if time_missing >= grace_period:
|
||||
logger.info(
|
||||
f"Deck card {doc_id} missing for {time_missing:.1f}s "
|
||||
f"(>{grace_period:.1f}s grace period), sending deletion"
|
||||
"Deck card %s missing for %ss (>%ss grace period), sending deletion",
|
||||
doc_id,
|
||||
format(time_missing, ".1f"),
|
||||
format(grace_period, ".1f"),
|
||||
)
|
||||
await send_stream.send(
|
||||
DocumentTask(
|
||||
@@ -1078,7 +1124,8 @@ async def scan_deck_cards(
|
||||
del _potentially_deleted[doc_key]
|
||||
else:
|
||||
logger.debug(
|
||||
f"Deck card {doc_id} missing for first time, starting grace period"
|
||||
"Deck card %s missing for first time, starting grace period",
|
||||
doc_id,
|
||||
)
|
||||
_potentially_deleted[doc_key] = current_time
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ async def compute_pca_coordinates(
|
||||
if embedding_dim is None:
|
||||
return {"coordinates_3d": [], "query_coords": []}
|
||||
|
||||
logger.info(f"Detected embedding dimension: {embedding_dim}")
|
||||
logger.info("Detected embedding dimension: %s", embedding_dim)
|
||||
|
||||
# Build chunk vectors array in search_results order (1:1 mapping)
|
||||
chunk_vectors = []
|
||||
@@ -103,7 +103,7 @@ async def compute_pca_coordinates(
|
||||
else:
|
||||
# Chunk not found in vectors (shouldn't happen)
|
||||
logger.warning(
|
||||
f"Chunk {chunk_key} not found in fetched vectors, using zero vector"
|
||||
"Chunk %s not found in fetched vectors, using zero vector", chunk_key
|
||||
)
|
||||
chunk_vectors.append(np.zeros(embedding_dim))
|
||||
|
||||
@@ -129,17 +129,19 @@ async def compute_pca_coordinates(
|
||||
if zero_norm_mask.any():
|
||||
zero_indices = np.where(zero_norm_mask)[0]
|
||||
logger.warning(
|
||||
f"Found {zero_norm_mask.sum()} zero-norm vectors at indices "
|
||||
f"{zero_indices.tolist()}. Replacing with small epsilon to avoid "
|
||||
"division by zero."
|
||||
"Found %s zero-norm vectors at indices %s. Replacing with small epsilon to avoid division by zero.",
|
||||
zero_norm_mask.sum(),
|
||||
zero_indices.tolist(),
|
||||
)
|
||||
# Replace zero norms with small epsilon to avoid NaN
|
||||
norms[zero_norm_mask] = 1e-10
|
||||
|
||||
all_vectors_normalized = all_vectors / norms
|
||||
logger.info(
|
||||
f"Normalized vectors: query_norm={norms[-1][0]:.3f}, "
|
||||
f"doc_norm_range=[{norms[:-1].min():.3f}, {norms[:-1].max():.3f}]"
|
||||
"Normalized vectors: query_norm=%s, doc_norm_range=[%s, %s]",
|
||||
format(norms[-1][0], ".3f"),
|
||||
format(norms[:-1].min(), ".3f"),
|
||||
format(norms[:-1].max(), ".3f"),
|
||||
)
|
||||
|
||||
# Apply PCA dimensionality reduction (768-dim → 3D)
|
||||
@@ -161,9 +163,9 @@ async def compute_pca_coordinates(
|
||||
if nan_mask.any():
|
||||
nan_rows = np.where(nan_mask.any(axis=1))[0]
|
||||
logger.error(
|
||||
f"Found NaN values in PCA output at {len(nan_rows)} points: "
|
||||
f"{nan_rows.tolist()[:10]}. Replacing NaN with 0.0 to prevent "
|
||||
"JSON serialization error."
|
||||
"Found NaN values in PCA output at %s points: %s. Replacing NaN with 0.0 to prevent JSON serialization error.",
|
||||
len(nan_rows),
|
||||
nan_rows.tolist()[:10],
|
||||
)
|
||||
# Replace NaN with 0 to allow JSON serialization
|
||||
coords_3d = np.nan_to_num(coords_3d, nan=0.0)
|
||||
@@ -174,9 +176,10 @@ async def compute_pca_coordinates(
|
||||
chunk_coords_3d = coords_3d[:-1] # All but last are chunks
|
||||
|
||||
logger.info(
|
||||
f"PCA explained variance: PC1={pca.explained_variance_ratio_[0]:.3f}, "
|
||||
f"PC2={pca.explained_variance_ratio_[1]:.3f}, "
|
||||
f"PC3={pca.explained_variance_ratio_[2]:.3f}"
|
||||
"PCA explained variance: PC1=%s, PC2=%s, PC3=%s",
|
||||
format(pca.explained_variance_ratio_[0], ".3f"),
|
||||
format(pca.explained_variance_ratio_[1], ".3f"),
|
||||
format(pca.explained_variance_ratio_[2], ".3f"),
|
||||
)
|
||||
|
||||
# Coordinates already match search_results order (1:1 mapping)
|
||||
|
||||
Reference in New Issue
Block a user