chore: convert mindmodel from YAML to markdown and clean up
Delete 17 malformed YAML constraint files and 10 stale numbered constraint files. Convert domain glossary, patterns, stack, and anti-patterns to markdown format. Update manifest.yaml to reference new markdown files.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: DuckDB Access Pattern
|
||||
category: patterns
|
||||
---
|
||||
# DuckDB Access Pattern
|
||||
|
||||
## Rules
|
||||
|
||||
- Prefer using read_only=True for compute-only subprocesses (e.g., SVD compute) to allow concurrent readers.
|
||||
- Prefer "with duckdb.connect(db_path, read_only=True) as conn" for scoped connections so conn.close() is automatic.
|
||||
- If a long-lived connection is created at module level, provide explicit close() or ensure operation is safe for Streamlit's lifecycle.
|
||||
- Prefer parameterizing db_path in pipelines and creating connections locally (avoid global connections that cross threads).
|
||||
|
||||
## Examples
|
||||
|
||||
### database.py - Explicit connect/close for schema init
|
||||
|
||||
```python
|
||||
conn = duckdb.connect(self.db_path)
|
||||
...
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fused_embeddings (
|
||||
id INTEGER DEFAULT nextval('fused_embeddings_id_seq'),
|
||||
motion_id INTEGER NOT NULL,
|
||||
window_id TEXT NOT NULL,
|
||||
vector JSON NOT NULL,
|
||||
svd_dims INTEGER NOT NULL,
|
||||
text_dims INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
""")
|
||||
conn.close()
|
||||
```
|
||||
|
||||
### pipeline/svd_pipeline.py - Read-only connection
|
||||
|
||||
```python
|
||||
conn = duckdb.connect(db_path, read_only=True)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT motion_id, mp_name, vote FROM mp_votes WHERE date BETWEEN ? AND ?",
|
||||
(start_date, end_date),
|
||||
).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
### similarity/compute.py - Preferred 'with' context
|
||||
|
||||
```python
|
||||
try:
|
||||
import duckdb
|
||||
except Exception:
|
||||
logger.exception("duckdb import failed; cannot load vectors")
|
||||
return 0
|
||||
|
||||
with duckdb.connect(db.db_path) as conn:
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Connection without closure
|
||||
|
||||
```python
|
||||
# BAD: connection may leak if exception occurs before explicit close
|
||||
conn = duckdb.connect(db_path)
|
||||
rows = conn.execute("SELECT ...").fetchall()
|
||||
# missing finally/close
|
||||
```
|
||||
|
||||
**Remediation**: Use "with" context or ensure conn.close() in finally block.
|
||||
|
||||
### Bad: Parallel write connections
|
||||
|
||||
**Problem**: Opening write connections from many parallel workers without coordination.
|
||||
|
||||
**Remediation**: Open read_only for compute processes and centralize writes via short-lived connections or a single writer worker.
|
||||
@@ -1,70 +0,0 @@
|
||||
name: duckdb_access
|
||||
|
||||
rules:
|
||||
- Prefer using read_only=True for compute-only subprocesses (e.g., SVD compute) to allow concurrent readers.
|
||||
- Prefer "with duckdb.connect(db_path, read_only=True) as conn" for scoped connections so conn.close() is automatic.
|
||||
- If a long-lived connection is created at module level, provide explicit close() or ensure operation is safe for Streamlit's lifecycle.
|
||||
- Prefer parameterizing db_path in pipelines and creating connections locally (avoid global connections that cross threads).
|
||||
|
||||
examples:
|
||||
- path: database.py
|
||||
excerpt: |
|
||||
```python
|
||||
conn = duckdb.connect(self.db_path)
|
||||
...
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fused_embeddings (
|
||||
id INTEGER DEFAULT nextval('fused_embeddings_id_seq'),
|
||||
motion_id INTEGER NOT NULL,
|
||||
window_id TEXT NOT NULL,
|
||||
vector JSON NOT NULL,
|
||||
svd_dims INTEGER NOT NULL,
|
||||
text_dims INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
""")
|
||||
conn.close()
|
||||
```
|
||||
note: explicit connect/close used when initializing schema
|
||||
|
||||
- path: pipeline/svd_pipeline.py
|
||||
excerpt: |
|
||||
```python
|
||||
conn = duckdb.connect(db_path, read_only=True)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT motion_id, mp_name, vote FROM mp_votes WHERE date BETWEEN ? AND ?",
|
||||
(start_date, end_date),
|
||||
).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
```
|
||||
note: read_only connection used for compute-heavy worker
|
||||
|
||||
- path: similarity/compute.py
|
||||
excerpt: |
|
||||
```python
|
||||
try:
|
||||
import duckdb
|
||||
except Exception:
|
||||
logger.exception("duckdb import failed; cannot load vectors")
|
||||
return 0
|
||||
|
||||
with duckdb.connect(db.db_path) as conn:
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
```
|
||||
note: preferred 'with' context for automatic close
|
||||
|
||||
anti_patterns:
|
||||
- Bad: creating a connection without closure in a long-running process
|
||||
remediation: use "with" context or ensure conn.close() in finally block
|
||||
example: |
|
||||
```python
|
||||
# BAD: connection may leak if exception occurs before explicit close
|
||||
conn = duckdb.connect(db_path)
|
||||
rows = conn.execute("SELECT ...").fetchall()
|
||||
# missing finally/close
|
||||
```
|
||||
- Bad: Opening write connections from many parallel workers without coordination
|
||||
remediation: open read_only for compute processes and centralize writes via short-lived connections or a single writer worker.
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Embeddings Similarity Pipeline
|
||||
category: patterns
|
||||
---
|
||||
# Embeddings Similarity Pipeline
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep embedding calls batched where possible; fallback to per-item attempts on persistent batch failure.
|
||||
- Store raw embeddings, SVD vectors, and fused_embeddings separately; fused_embeddings are typically concatenation [svd + text].
|
||||
- Compute similarity as normalized cosine on padded vectors; record top-k neighbors in similarity_cache.
|
||||
- Use read_only DuckDB connections in compute workers to allow parallel runs.
|
||||
|
||||
## Examples
|
||||
|
||||
### pipeline/ai_provider_wrapper.py - Batched embed + fallback
|
||||
|
||||
```python
|
||||
for start in range(0, len(texts), batch_size):
|
||||
chunk = texts[start : start + batch_size]
|
||||
resp = _post_with_retries("/embeddings", json={"model": model, "input": chunk})
|
||||
...
|
||||
for j in range(i, end):
|
||||
t = texts[j]
|
||||
single, single_exc = _attempt_batch([t], j)
|
||||
if single:
|
||||
results[j] = single[0]
|
||||
```
|
||||
|
||||
### pipeline/fusion.py - Concatenation and storage
|
||||
|
||||
```python
|
||||
try:
|
||||
svd_vec = json.loads(svd_json)
|
||||
except Exception:
|
||||
_logger.exception("Invalid SVD vector JSON for entity %s", entity_id)
|
||||
skipped_missing_svd += 1
|
||||
continue
|
||||
...
|
||||
fused = list(svd_vec) + list(text_vec)
|
||||
res = db.store_fused_embedding(
|
||||
int(entity_id),
|
||||
window_id,
|
||||
fused,
|
||||
svd_dims=len(svd_vec),
|
||||
text_dims=len(text_vec),
|
||||
)
|
||||
```
|
||||
|
||||
### similarity/compute.py - Normalized cosine similarity
|
||||
|
||||
```python
|
||||
# Normalize rows
|
||||
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
|
||||
norms[norms == 0] = 1.0
|
||||
normalized = matrix / norms
|
||||
sim = normalized @ normalized.T
|
||||
...
|
||||
# pick top-k neighbors and write to similarity_cache
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Assuming consistent vector length
|
||||
|
||||
**Problem**: Assuming consistent vector length without checks leads to shape errors.
|
||||
|
||||
**Remediation**: Detect inconsistent lengths, pad with zeros, and log a warning (as seen in compute.py).
|
||||
|
||||
### Bad: Inline heavy computation in UI
|
||||
|
||||
**Problem**: Recomputing heavy pipelines inline in UI requests.
|
||||
|
||||
**Remediation**: Schedule heavy work in scripts/subprocesses and read precomputed results in UI.
|
||||
@@ -1,63 +0,0 @@
|
||||
name: embeddings_similarity_pipeline
|
||||
|
||||
rules:
|
||||
- Keep embedding calls batched where possible; fallback to per-item attempts on persistent batch failure.
|
||||
- Store raw embeddings, SVD vectors, and fused_embeddings separately; fused_embeddings are typically concatenation [svd + text].
|
||||
- Compute similarity as normalized cosine on padded vectors; record top-k neighbors in similarity_cache.
|
||||
- Use read_only DuckDB connections in compute workers to allow parallel runs.
|
||||
|
||||
examples:
|
||||
- path: pipeline/ai_provider_wrapper.py
|
||||
excerpt: |
|
||||
```python
|
||||
for start in range(0, len(texts), batch_size):
|
||||
chunk = texts[start : start + batch_size]
|
||||
resp = _post_with_retries("/embeddings", json={"model": model, "input": chunk})
|
||||
...
|
||||
for j in range(i, end):
|
||||
t = texts[j]
|
||||
single, single_exc = _attempt_batch([t], j)
|
||||
if single:
|
||||
results[j] = single[0]
|
||||
```
|
||||
note: batched embed + fallback per-item retry
|
||||
|
||||
- path: pipeline/fusion.py
|
||||
excerpt: |
|
||||
```python
|
||||
try:
|
||||
svd_vec = json.loads(svd_json)
|
||||
except Exception:
|
||||
_logger.exception("Invalid SVD vector JSON for entity %s", entity_id)
|
||||
skipped_missing_svd += 1
|
||||
continue
|
||||
...
|
||||
fused = list(svd_vec) + list(text_vec)
|
||||
res = db.store_fused_embedding(
|
||||
int(entity_id),
|
||||
window_id,
|
||||
fused,
|
||||
svd_dims=len(svd_vec),
|
||||
text_dims=len(text_vec),
|
||||
)
|
||||
```
|
||||
note: concatenation of vectors and storage via MotionDatabase
|
||||
|
||||
- path: similarity/compute.py
|
||||
excerpt: |
|
||||
```python
|
||||
# Normalize rows
|
||||
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
|
||||
norms[norms == 0] = 1.0
|
||||
normalized = matrix / norms
|
||||
sim = normalized @ normalized.T
|
||||
...
|
||||
# pick top-k neighbors and write to similarity_cache
|
||||
```
|
||||
note: numeric pipeline and padding to consistent dimensionality
|
||||
|
||||
anti_patterns:
|
||||
- Bad: Assuming consistent vector length without checks (leads to shape errors).
|
||||
remediation: Detect inconsistent lengths, pad with zeros, and log a warning (as seen in compute.py).
|
||||
- Bad: Recomputing heavy pipelines inline in UI requests.
|
||||
remediation: schedule heavy work in scripts/subprocesses and read precomputed results in UI.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Error Handling Pattern
|
||||
category: patterns
|
||||
---
|
||||
# Error Handling Pattern
|
||||
|
||||
## Rules
|
||||
|
||||
- Use explicit exceptions for domain/error classification (e.g., ProviderError, ValueError).
|
||||
- Prefer logging.exception when catching an exception where stack trace is useful.
|
||||
- Avoid broad except: clauses that swallow exceptions; if broad except is used for "best-effort" fallback, log at warning and include original exception context.
|
||||
- For public library-like functions, prefer raising typed exceptions instead of returning magic values ([], False) — only return safe defaults where documented.
|
||||
|
||||
## Examples
|
||||
|
||||
### ai_provider.py - Network error to ProviderError
|
||||
|
||||
```python
|
||||
except requests.ConnectionError as exc:
|
||||
if attempt == retries:
|
||||
raise ProviderError(
|
||||
f"Connection error when calling provider: {exc}"
|
||||
) from exc
|
||||
...
|
||||
```
|
||||
|
||||
### pipeline/ai_provider_wrapper.py - Best-effort with logging
|
||||
|
||||
```python
|
||||
except Exception:
|
||||
_logger.exception("Failed to append audit event for embedding failure")
|
||||
results[j] = None
|
||||
```
|
||||
|
||||
### similarity/compute.py - Defensive import handling
|
||||
|
||||
```python
|
||||
try:
|
||||
import duckdb
|
||||
except Exception:
|
||||
logger.exception("duckdb import failed; cannot load vectors")
|
||||
return 0
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Silent exception swallowing
|
||||
|
||||
```python
|
||||
try:
|
||||
do_work()
|
||||
except Exception:
|
||||
return []
|
||||
# BAD: hides the root cause and returns an ambiguous default
|
||||
```
|
||||
|
||||
**Remediation**: Narrow exception types or at minimum log.exception() and re-raise or convert to a domain error if truly handled.
|
||||
|
||||
### Bad: Mixing print() and logging
|
||||
|
||||
**Problem**: Mixing print() and logging for errors.
|
||||
|
||||
**Remediation**: Replace print() calls with logger.* calls; use structured logging configuration.
|
||||
@@ -1,54 +0,0 @@
|
||||
name: error_handling
|
||||
|
||||
rules:
|
||||
- Use explicit exceptions for domain/error classification (e.g., ProviderError, ValueError).
|
||||
- Prefer logging.exception when catching an exception where stack trace is useful.
|
||||
- Avoid broad except: clauses that swallow exceptions; if broad except is used for "best-effort" fallback, log at warning and include original exception context.
|
||||
- For public library-like functions, prefer raising typed exceptions instead of returning magic values ([], False) — only return safe defaults where documented.
|
||||
|
||||
examples:
|
||||
- path: ai_provider.py
|
||||
excerpt: |
|
||||
```python
|
||||
except requests.ConnectionError as exc:
|
||||
if attempt == retries:
|
||||
raise ProviderError(
|
||||
f"Connection error when calling provider: {exc}"
|
||||
) from exc
|
||||
...
|
||||
```
|
||||
note: mapping network error to ProviderError with re-raise chaining
|
||||
|
||||
- path: pipeline/ai_provider_wrapper.py
|
||||
excerpt: |
|
||||
```python
|
||||
except Exception:
|
||||
_logger.exception("Failed to append audit event for embedding failure")
|
||||
results[j] = None
|
||||
```
|
||||
note: logs and assigns None for failure; fallback behavior documented earlier in wrapper rule
|
||||
|
||||
- path: similarity/compute.py
|
||||
excerpt: |
|
||||
```python
|
||||
try:
|
||||
import duckdb
|
||||
except Exception:
|
||||
logger.exception("duckdb import failed; cannot load vectors")
|
||||
return 0
|
||||
```
|
||||
note: defensive import handling and early return on failure
|
||||
|
||||
anti_patterns:
|
||||
- Bad: Broad except without logging and without re-raising (silently hides bugs)
|
||||
remediation: Narrow exception types or at minimum log.exception() and re-raise or convert to a domain error if truly handled.
|
||||
example: |
|
||||
```python
|
||||
try:
|
||||
do_work()
|
||||
except Exception:
|
||||
return []
|
||||
# BAD: hides the root cause and returns an ambiguous default
|
||||
```
|
||||
- Bad: Mixing print() and logging for errors
|
||||
remediation: Replace print() calls with logger.* calls; use structured logging configuration.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Module Singletons Pattern
|
||||
category: patterns
|
||||
---
|
||||
# Module Singletons Pattern
|
||||
|
||||
## Rules
|
||||
|
||||
- Module-level singletons (e.g., db = MotionDatabase()) are acceptable but should be created carefully:
|
||||
- Avoid expensive initialization at import time.
|
||||
- Provide a way to construct with a test DB path or to reinitialize in tests.
|
||||
- If a singleton holds resources (DB connections, sessions), ensure safe shutdown on program exit.
|
||||
|
||||
## Examples
|
||||
|
||||
### database.py - Safe class initialization
|
||||
|
||||
```python
|
||||
class MotionDatabase:
|
||||
def __init__(self, db_path: str = config.DATABASE_PATH):
|
||||
self.db_path = db_path
|
||||
# If duckdb is not available, operate in lightweight file-backed mode
|
||||
self._file_mode = duckdb is None
|
||||
self._init_database()
|
||||
```
|
||||
|
||||
### similarity/lookup.py - Local instances
|
||||
|
||||
```python
|
||||
db = MotionDatabase(db_path=db_path) if db_path else MotionDatabase()
|
||||
if hasattr(db, "get_cached_similarities"):
|
||||
rows = db.get_cached_similarities(...)
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Heavy initialization at import time
|
||||
|
||||
**Problem**: Creating connections and performing heavy schema migrations during import.
|
||||
|
||||
**Remediation**: Move heavy init to an explicit initialize() method and keep import fast.
|
||||
@@ -1,33 +0,0 @@
|
||||
name: module_singletons
|
||||
|
||||
rules:
|
||||
- Module-level singletons (e.g., db = MotionDatabase()) are acceptable but should be created carefully:
|
||||
- Avoid expensive initialization at import time.
|
||||
- Provide a way to construct with a test DB path or to reinitialize in tests.
|
||||
- If a singleton holds resources (DB connections, sessions), ensure safe shutdown on program exit.
|
||||
|
||||
examples:
|
||||
- path: database.py
|
||||
excerpt: |
|
||||
```python
|
||||
class MotionDatabase:
|
||||
def __init__(self, db_path: str = config.DATABASE_PATH):
|
||||
self.db_path = db_path
|
||||
# If duckdb is not available, operate in lightweight file-backed mode
|
||||
self._file_mode = duckdb is None
|
||||
self._init_database()
|
||||
```
|
||||
note: class is safe to instantiate and creates DB at init; consider lazy init if heavy
|
||||
|
||||
- path: similarity/lookup.py
|
||||
excerpt: |
|
||||
```python
|
||||
db = MotionDatabase(db_path=db_path) if db_path else MotionDatabase()
|
||||
if hasattr(db, "get_cached_similarities"):
|
||||
rows = db.get_cached_similarities(...)
|
||||
```
|
||||
note: consumers create local MotionDatabase instances, not relying on a single global
|
||||
|
||||
anti_patterns:
|
||||
- Bad: Creating connections and performing heavy schema migrations during import
|
||||
remediation: Move heavy init to an explicit initialize() method and keep import fast.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Requests HTTP Pattern
|
||||
category: patterns
|
||||
---
|
||||
# Requests HTTP Pattern
|
||||
|
||||
## Rules
|
||||
|
||||
- Reuse requests.Session when making multiple calls to the same host to benefit from connection pooling.
|
||||
- Wrap outbound HTTP calls with retry/backoff logic and respect Retry-After on 429.
|
||||
- Treat 5xx as transient and retry; surface 4xx as configuration/client errors (do not retry unless 429).
|
||||
- Raise or wrap non-OK responses into domain ProviderError to make behavior consistent across the codebase.
|
||||
|
||||
## Examples
|
||||
|
||||
### ai_provider.py - 429 handling with Retry-After
|
||||
|
||||
```python
|
||||
resp = requests.post(url, json=json, headers=headers, timeout=10)
|
||||
...
|
||||
if getattr(resp, "status_code", 0) == 429:
|
||||
if attempt == retries:
|
||||
raise ProviderError(f"Provider returned HTTP {resp.status_code}")
|
||||
retry_after = None
|
||||
raw = resp.headers.get("Retry-After") if getattr(resp, "headers", None) else None
|
||||
if raw:
|
||||
try:
|
||||
retry_after = int(raw)
|
||||
except Exception:
|
||||
...
|
||||
if retry_after is not None:
|
||||
time.sleep(retry_after)
|
||||
continue
|
||||
```
|
||||
|
||||
### api_client.py - Session + raise_for_status
|
||||
|
||||
```python
|
||||
response = self.session.get(
|
||||
base_url, params=params, timeout=config.API_TIMEOUT
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
```
|
||||
|
||||
### pipeline/ai_provider_wrapper.py - Retry/backoff wrapper
|
||||
|
||||
```python
|
||||
def _attempt_batch(chunk_texts, start_index):
|
||||
backoff = 0.5
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
emb_chunk = _embedder(
|
||||
chunk_texts, model=model, batch_size=len(chunk_texts)
|
||||
)
|
||||
return emb_chunk, None
|
||||
except Exception as exc:
|
||||
if attempt == retries:
|
||||
break
|
||||
sleep = backoff * (2 ** (attempt - 1))
|
||||
time.sleep(sleep)
|
||||
continue
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Silent exception swallowing
|
||||
|
||||
**Problem**: Blindly catching all requests exceptions and returning empty response.
|
||||
|
||||
**Remediation**: Map network exceptions to retryable vs terminal (ProviderError) and log details.
|
||||
|
||||
### Bad: Using print() for errors
|
||||
|
||||
**Problem**: Using print() for network errors instead of structured logging.
|
||||
|
||||
**Remediation**: Use `_logger.exception()` instead (see api_client.py needs fixing).
|
||||
@@ -1,65 +0,0 @@
|
||||
name: requests_http
|
||||
|
||||
rules:
|
||||
- Reuse requests.Session when making multiple calls to the same host to benefit from connection pooling.
|
||||
- Wrap outbound HTTP calls with retry/backoff logic and respect Retry-After on 429.
|
||||
- Treat 5xx as transient and retry; surface 4xx as configuration/client errors (do not retry unless 429).
|
||||
- Raise or wrap non-OK responses into domain ProviderError to make behavior consistent across the codebase.
|
||||
|
||||
examples:
|
||||
- path: ai_provider.py
|
||||
excerpt: |
|
||||
```python
|
||||
resp = requests.post(url, json=json, headers=headers, timeout=10)
|
||||
...
|
||||
if getattr(resp, "status_code", 0) == 429:
|
||||
if attempt == retries:
|
||||
raise ProviderError(f"Provider returned HTTP {resp.status_code}")
|
||||
retry_after = None
|
||||
raw = resp.headers.get("Retry-After") if getattr(resp, "headers", None) else None
|
||||
if raw:
|
||||
try:
|
||||
retry_after = int(raw)
|
||||
except Exception:
|
||||
...
|
||||
if retry_after is not None:
|
||||
time.sleep(retry_after)
|
||||
continue
|
||||
```
|
||||
note: explicit handling of 429 and Retry-After
|
||||
|
||||
- path: api_client.py
|
||||
excerpt: |
|
||||
```python
|
||||
response = self.session.get(
|
||||
base_url, params=params, timeout=config.API_TIMEOUT
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
```
|
||||
note: uses session + raise_for_status() to surface HTTP errors
|
||||
|
||||
- path: pipeline/ai_provider_wrapper.py
|
||||
excerpt: |
|
||||
```python
|
||||
def _attempt_batch(chunk_texts, start_index):
|
||||
backoff = 0.5
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
emb_chunk = _embedder(
|
||||
chunk_texts, model=model, batch_size=len(chunk_texts)
|
||||
)
|
||||
return emb_chunk, None
|
||||
except Exception as exc:
|
||||
if attempt == retries:
|
||||
break
|
||||
sleep = backoff * (2 ** (attempt - 1))
|
||||
time.sleep(sleep)
|
||||
continue
|
||||
```
|
||||
note: wrapper adds retry/backoff and per-item fallback
|
||||
|
||||
anti_patterns:
|
||||
- Bad: Blindly catching all requests exceptions and returning empty response
|
||||
remediation: map network exceptions to retryable vs terminal (ProviderError) and log details.
|
||||
- Bad: Using print() for network errors instead of structured logging (see api_client.py where print() is used; prefer logging).
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Validation Pattern
|
||||
category: patterns
|
||||
---
|
||||
# Validation Pattern
|
||||
|
||||
## Rules
|
||||
|
||||
- Validate inputs early and raise ValueError or domain-specific exceptions (ProviderError) for invalid contract inputs.
|
||||
- Tests should assert that invalid inputs raise the expected exceptions.
|
||||
- Use explicit checks for types and shapes on public APIs (e.g., ensure text is str before embedding).
|
||||
|
||||
## Examples
|
||||
|
||||
### ai_provider.py - Type validation
|
||||
|
||||
```python
|
||||
if not isinstance(text, str):
|
||||
raise ProviderError("text must be a string")
|
||||
```
|
||||
|
||||
### pipeline/ai_provider_wrapper.py - Defensive empty handling
|
||||
|
||||
```python
|
||||
if not texts:
|
||||
return []
|
||||
if motion_ids is None:
|
||||
motion_ids = [None for _ in texts]
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Bad: Invalid values into computation
|
||||
|
||||
**Problem**: Allowing invalid values to propagate into heavy computation (e.g., non-string into embedding pipeline).
|
||||
|
||||
**Remediation**: Fail fast with a typed exception and add unit tests to cover validations.
|
||||
@@ -1,29 +0,0 @@
|
||||
name: validation
|
||||
|
||||
rules:
|
||||
- Validate inputs early and raise ValueError or domain-specific exceptions (ProviderError) for invalid contract inputs.
|
||||
- Tests should assert that invalid inputs raise the expected exceptions.
|
||||
- Use explicit checks for types and shapes on public APIs (e.g., ensure text is str before embedding).
|
||||
|
||||
examples:
|
||||
- path: ai_provider.py
|
||||
excerpt: |
|
||||
```python
|
||||
if not isinstance(text, str):
|
||||
raise ProviderError("text must be a string")
|
||||
```
|
||||
note: explicit type validation before network call
|
||||
|
||||
- path: pipeline/ai_provider_wrapper.py
|
||||
excerpt: |
|
||||
```python
|
||||
if not texts:
|
||||
return []
|
||||
if motion_ids is None:
|
||||
motion_ids = [None for _ in texts]
|
||||
```
|
||||
note: defensive handling of empty inputs
|
||||
|
||||
anti_patterns:
|
||||
- Bad: Allowing invalid values to propagate into heavy computation (e.g., non-string into embedding pipeline).
|
||||
remediation: Fail fast with a typed exception and add unit tests to cover validations.
|
||||
Reference in New Issue
Block a user