Skip to content

Commit 137f17e

Browse files
Merge branch 'main' into feat/586-users
2 parents 1bb2424 + 2d1aa47 commit 137f17e

38 files changed

Lines changed: 1709 additions & 248 deletions

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ OpenCRE_gspread_Auth=path/to/credentials.json
7474
# application/utils/librarian/config_loader.py for validation)
7575

7676
# Retrieval backend: in_memory (sklearn cosine; SQLite dev/CI/harness) or
77-
# pgvector (Postgres vector column — pending prod extension + mentor OK).
77+
# pgvector (Postgres ``embedding_vec`` after Alembic c7d8e9f0a1b2 / #977).
7878
CRE_LIBRARIAN_RETRIEVER_BACKEND=in_memory
7979
# Candidate shortlist size produced by C.1 and reranked by C.2 (W4).
8080
CRE_LIBRARIAN_TOP_K_RETRIEVAL=20

.github/workflows/linter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
python -m venv venv
4444
. ./venv/bin/activate
4545
pip install --upgrade pip setuptools
46-
pip install -r requirements.txt
46+
pip install -r requirements-dev.txt
4747
- name: OpenAPI guardrail
4848
run: make openapi-guardrail
4949
- name: Lint Code Base

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ Use Makefile targets — do not hand-roll `docker run`:
2525
```bash
2626
make docker-neo4j # Neo4j (7474/7687)
2727
make docker-redis # Redis Stack (6379/8001)
28-
make docker-postgres # local Postgres (5432, cre/password)
28+
make docker-postgres # local Postgres+pgvector (5432, cre/password)
2929
make start-containers # neo4j + redis only
3030
make migrate-upgrade # after postgres is up
3131
```
3232

33-
Reset volumes: `make docker-neo4j-rm` / `make docker-redis-rm`.
33+
Reset volumes: `make docker-neo4j-rm` / `make docker-redis-rm` / `make docker-postgres-rm`.
34+
Local Postgres is **`pgvector/pgvector:pg16`** by default (`POSTGRES_IMAGE` override). Plain `postgres` images are not supported for app DB use — `embedding_vec` / chat / Librarian need the `vector` extension.
3435

3536
### Imports and Neo4j populate
3637

Makefile

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,48 @@ docker-redis:
2525
docker start cre-redis-stack 2>/dev/null ||\
2626
docker run -d --name cre-redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
2727

28+
# Local app DB — always pgvector (embedding_vec / Librarian / chat similarity).
29+
POSTGRES_IMAGE ?= pgvector/pgvector:pg16
30+
31+
docker-postgres-rm:
32+
-docker stop cre-postgres
33+
-docker rm -f cre-postgres
34+
2835
docker-postgres:
29-
docker start cre-postgres 2>/dev/null ||\
30-
docker run -d --name cre-postgres -e POSTGRES_PASSWORD=password -e POSTGRES_USER=cre -e POSTGRES_DB=cre -p 5432:5432 postgres
36+
@wanted="$(POSTGRES_IMAGE)"; \
37+
if docker inspect cre-postgres >/dev/null 2>&1; then \
38+
have=$$(docker inspect -f '{{.Config.Image}}' cre-postgres); \
39+
if [ "$$have" = "$$wanted" ]; then \
40+
docker start cre-postgres >/dev/null; \
41+
else \
42+
echo "Recreating cre-postgres (was $$have → $$wanted)"; \
43+
docker stop cre-postgres >/dev/null 2>&1 || true; \
44+
docker rm -f cre-postgres >/dev/null 2>&1 || true; \
45+
docker run -d --name cre-postgres \
46+
-e POSTGRES_PASSWORD=password \
47+
-e POSTGRES_USER=cre \
48+
-e POSTGRES_DB=cre \
49+
-p 5432:5432 \
50+
"$$wanted"; \
51+
fi; \
52+
else \
53+
docker run -d --name cre-postgres \
54+
-e POSTGRES_PASSWORD=password \
55+
-e POSTGRES_USER=cre \
56+
-e POSTGRES_DB=cre \
57+
-p 5432:5432 \
58+
"$$wanted"; \
59+
fi; \
60+
ready=0; \
61+
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do \
62+
if docker exec cre-postgres pg_isready -U cre -d cre >/dev/null 2>&1; then ready=1; break; fi; \
63+
sleep 1; \
64+
done; \
65+
if [ "$$ready" != "1" ]; then \
66+
echo "error: cre-postgres did not become ready within 30s" >&2; \
67+
exit 1; \
68+
fi; \
69+
docker exec cre-postgres psql -U cre -d cre -c "CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null
3170

3271
start-containers: docker-neo4j docker-redis
3372

@@ -69,7 +108,7 @@ cover:
69108
install-deps-python:
70109
[ -d "./venv" ] && . ./venv/bin/activate &&\
71110
pip install --upgrade pip setuptools &&\
72-
pip install -r requirements.txt
111+
pip install -r requirements-dev.txt
73112

74113
install-deps-typescript:
75114
(cd application/frontend && yarn install)

application/cmd/cre_main.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@
99
import requests
1010

1111
from collections import deque
12-
from typing import Any, Callable, Dict, List, Optional, Tuple
12+
from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
1313
import hashlib
1414
import json as _json
1515
from rq import Queue, job, exceptions
1616
from sqlalchemy import not_
1717

18-
from application.utils.external_project_parsers.base_parser import BaseParser
19-
from application.utils.external_project_parsers.parsers import master_spreadsheet_parser
2018
from application import create_app # type: ignore
2119
from application.config import CMDConfig
2220
from application.database import db
@@ -26,11 +24,12 @@
2624
from application.utils import spreadsheet as sheet_utils
2725
from application.utils import redis
2826
from application.utils import db_backend
29-
from alive_progress import alive_bar
30-
from application.prompt_client import prompt_client as prompt_client
3127
from application.utils import gap_analysis
3228
from application.utils import cres_csv_export
3329

30+
if TYPE_CHECKING:
31+
from application.prompt_client import prompt_client as prompt_client
32+
3433
logging.basicConfig()
3534
logger = logging.getLogger(__name__)
3635
logger.setLevel(logging.INFO)
@@ -469,6 +468,8 @@ def _standard_structure_fingerprint(resource_name: str) -> str:
469468
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
470469

471470
conn = redis.connect()
471+
from application.prompt_client import prompt_client as prompt_client
472+
472473
ph = prompt_client.PromptHandler(database=collection)
473474
importing_name = standard_entries[0].name
474475
effective_calculate_gap_analysis = (
@@ -541,7 +542,7 @@ def _standard_structure_fingerprint(resource_name: str) -> str:
541542
def parse_standards_from_spreadsheeet(
542543
cre_file: List[Dict[str, Any]],
543544
cache_location: str,
544-
prompt_handler: prompt_client.PromptHandler,
545+
prompt_handler: "prompt_client.PromptHandler",
545546
) -> None:
546547
"""given a csv with standards, build a list of standards in the db"""
547548
if not cre_file:
@@ -568,6 +569,10 @@ def parse_standards_from_spreadsheeet(
568569
from application.utils import import_pipeline
569570

570571
collection = db_connect(cache_location)
572+
from application.utils.external_project_parsers.parsers import (
573+
master_spreadsheet_parser,
574+
)
575+
571576
parse_result = master_spreadsheet_parser.MasterSpreadsheetParser.parse_rows(
572577
cre_file
573578
)
@@ -682,6 +687,8 @@ def download_gap_analysis_from_upstream(cache: str) -> None:
682687
pairs = [(sa, sb) for sa in standards for sb in standards if sa != sb]
683688

684689
if os.environ.get("BENCHMARK_MODE") == "1":
690+
from alive_progress import alive_bar
691+
685692
with alive_bar(len(pairs), title="Fetching upstream Gap Analysis") as bar:
686693
for sa, sb in pairs:
687694
res = requests.get(
@@ -846,11 +853,15 @@ def backfill_gap_analysis_only(
846853
jobs.append(j)
847854

848855
if jobs:
856+
from alive_progress import alive_bar
857+
849858
with alive_bar(
850859
len(jobs), title=f"GA batch {i // batch_size + 1}"
851860
) as bar:
852861
redis.wait_for_jobs(jobs, bar)
853862
else:
863+
from alive_progress import alive_bar
864+
854865
with alive_bar(len(batch), title=f"GA batch {i // batch_size + 1}") as bar:
855866
for sa, sb in batch:
856867
_compute_pair_direct(collection, sa, sb)
@@ -901,6 +912,8 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover
901912
cache.delete_nodes(args.delete_resource)
902913

903914
# individual resource importing
915+
from application.utils.external_project_parsers.base_parser import BaseParser
916+
904917
if args.zap_in:
905918
from application.utils.external_project_parsers.parsers import zap_alerts_parser
906919

@@ -1014,6 +1027,8 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover
10141027

10151028

10161029
def ai_client_init(database: db.Node_collection):
1030+
from application.prompt_client import prompt_client as prompt_client
1031+
10171032
return prompt_client.PromptHandler(database=database)
10181033

10191034

@@ -1053,6 +1068,8 @@ def prepare_for_review(cache: str) -> Tuple[str, str]:
10531068

10541069

10551070
def generate_embeddings(db_url: str) -> None:
1071+
from application.prompt_client import prompt_client as prompt_client
1072+
10561073
database = db_connect(path=db_url)
10571074
prompt_client.PromptHandler(database, load_all_embeddings=True)
10581075

@@ -1109,20 +1126,20 @@ def run_librarian(
11091126
"land W8); running in dry-run mode"
11101127
)
11111128

1129+
from application.prompt_client import prompt_client as prompt_client
1130+
11121131
cfg = load_config()
11131132
database = db_connect(path=cache_file)
11141133
ph = prompt_client.PromptHandler(database=database)
11151134

11161135
backend = RetrieverBackend(cfg.retriever_backend)
11171136
if backend is RetrieverBackend.pgvector:
1118-
dialect = database.session.connection().dialect.name
1119-
if dialect != "postgresql":
1120-
logger.warning(
1121-
"CRE_LIBRARIAN_RETRIEVER_BACKEND=pgvector selected on a %r "
1122-
"database, but the pgvector backend needs Postgres with the "
1123-
"embedding_vec column (lands W8) and will fail at retrieve() "
1124-
"time here. Set the backend to in_memory until then.",
1125-
dialect,
1137+
# Hard-fail: do not silently fall back to in_memory / sklearn CSV paths.
1138+
from application.database.pgvector_utils import fail_pgvector_unavailable
1139+
1140+
if not database.can_use_pgvector_similarity():
1141+
fail_pgvector_unavailable(
1142+
context="CRE_LIBRARIAN_RETRIEVER_BACKEND=pgvector"
11261143
)
11271144
# The CRE ids present in the hub are exactly the known ids the explicit
11281145
# resolver may auto-link to (W2 seeded this from the golden set; here it is
@@ -1136,13 +1153,16 @@ def run_librarian(
11361153
if backend is RetrieverBackend.in_memory
11371154
else None
11381155
)
1156+
pg_connection = None
1157+
if backend is RetrieverBackend.pgvector:
1158+
pg_connection = database.session.connection()
11391159
retriever = build_retriever(
11401160
backend,
11411161
embed_fn=ph.get_text_embeddings,
11421162
top_k=cfg.top_k_retrieval,
11431163
threshold=cfg.link_threshold,
11441164
pool=pool,
1145-
connection=database.session.connection(),
1165+
connection=pg_connection,
11461166
)
11471167

11481168
# C.2 reranker: reads each (section, candidate-CRE) pair together and
@@ -1207,6 +1227,8 @@ def run_librarian(
12071227

12081228
def regenerate_embeddings(db_url: str) -> None:
12091229
"""Wipe all embedding rows, then rebuild (CRE + every node type) like ``--generate_embeddings``."""
1230+
from application.prompt_client import prompt_client as prompt_client
1231+
12101232
database = db_connect(path=db_url)
12111233
removed = database.delete_all_embeddings()
12121234
logger.info("Removed %s embedding rows; rebuilding embeddings", removed)

0 commit comments

Comments
 (0)