Skip to content

Commit b6290af

Browse files
Merge branch 'main' into review/issue-471-ai-resource-importers
2 parents a11c5af + 6dcc3d2 commit b6290af

23 files changed

Lines changed: 1273 additions & 109 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ LOGIN_ALLOWED_DOMAINS=example.com
3838
# GCP
3939

4040
GCP_NATIVE=false
41+
GEMINI_API_KEY=your-gemini-api-key
42+
VERTEX_CHAT_MODEL=gemini-2.0-flash
43+
VERTEX_EMBED_CONTENT_MODEL=embedding-001
4144

4245
# Spreadsheet Auth
4346

Makefile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ 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+
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
31+
2832
start-containers: docker-neo4j docker-redis
2933

3034
start-worker:
@@ -132,4 +136,32 @@ import-neo4j:
132136
backfill-gap-analysis:
133137
RUN_COUNT=8 bash ./scripts/backfill_gap_analysis.sh
134138

139+
sync-gap-analysis-table-local:
140+
[ -d "./venv" ] && . ./venv/bin/activate &&\
141+
python scripts/sync_gap_analysis_table.py \
142+
--from-sqlite "$(CURDIR)/standards_cache.sqlite" \
143+
--to-postgres "postgresql://cre:password@127.0.0.1:5432/cre" \
144+
--require-local-destination
145+
146+
verify-ga-complete-prod:
147+
[ -d "./venv" ] && . ./venv/bin/activate &&\
148+
python scripts/verify_ga_completeness.py \
149+
--base-url "https://opencre.org" \
150+
--output-json "$(CURDIR)/tmp/prod-ga-completeness.json"
151+
152+
verify-ga-parity-local:
153+
@[ -d "./.venv" ] && . ./.venv/bin/activate || ([ -d "./venv" ] && . ./venv/bin/activate); \
154+
export CRE_CACHE_FILE="$${CRE_CACHE_FILE:-postgresql://cre:password@127.0.0.1:5432/cre}"; \
155+
export NEO4J_URL="$${NEO4J_URL:-bolt://neo4j:password@127.0.0.1:7687}"; \
156+
export PYTHONPATH="$(CURDIR)"; \
157+
python scripts/verify_ga_postgres_neo_parity.py --output-json "$(CURDIR)/tmp/local-ga-parity.json"
158+
159+
backfill-gap-analysis-sync:
160+
@[ -d "./.venv" ] && . ./.venv/bin/activate || ([ -d "./venv" ] && . ./venv/bin/activate); \
161+
export FLASK_APP="$(CURDIR)/cre.py"; \
162+
export CRE_CACHE_FILE="$${CRE_CACHE_FILE:-postgresql://cre:password@127.0.0.1:5432/cre}"; \
163+
export NEO4J_URL="$${NEO4J_URL:-bolt://neo4j:password@127.0.0.1:7687}"; \
164+
python cre.py --cache_file "$$CRE_CACHE_FILE" --populate_neo4j_db && \
165+
python cre.py --cache_file "$$CRE_CACHE_FILE" --ga_backfill_missing --ga_backfill_no_queue
166+
135167
all: clean lint test dev dev-run

application/cmd/cre_main.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import hashlib
1414
import json as _json
1515
from rq import Queue, job, exceptions
16+
from sqlalchemy import not_
1617

1718
from application.utils.external_project_parsers.base_parser import BaseParser
1819
from application.utils.external_project_parsers.parsers import master_spreadsheet_parser
@@ -649,11 +650,15 @@ def download_gap_analysis_from_upstream(cache: str) -> None:
649650
)
650651
if res.status_code == 200:
651652
tojson = res.json()
652-
if tojson.get("result"):
653-
cache_key = gap_analysis.make_resources_key([sa, sb])
654-
collection.add_gap_analysis_result(
655-
cache_key, _json.dumps({"result": tojson.get("result")})
656-
)
653+
if "result" not in tojson:
654+
continue
655+
payload = _json.dumps({"result": tojson.get("result")})
656+
if not gap_analysis.primary_gap_analysis_payload_is_material(
657+
payload
658+
):
659+
continue
660+
cache_key = gap_analysis.make_resources_key([sa, sb])
661+
collection.add_gap_analysis_result(cache_key, payload)
657662
bar()
658663
else:
659664
for sa, sb in pairs:
@@ -662,18 +667,33 @@ def download_gap_analysis_from_upstream(cache: str) -> None:
662667
)
663668
if res.status_code == 200:
664669
tojson = res.json()
665-
if tojson.get("result"):
666-
cache_key = gap_analysis.make_resources_key([sa, sb])
667-
collection.add_gap_analysis_result(
668-
cache_key, _json.dumps({"result": tojson.get("result")})
669-
)
670+
if "result" not in tojson:
671+
continue
672+
payload = _json.dumps({"result": tojson.get("result")})
673+
if not gap_analysis.primary_gap_analysis_payload_is_material(
674+
payload
675+
):
676+
continue
677+
cache_key = gap_analysis.make_resources_key([sa, sb])
678+
collection.add_gap_analysis_result(cache_key, payload)
670679

671680

672681
def _missing_ga_pairs(collection: db.Node_collection) -> List[Tuple[str, str]]:
673-
standards = sorted(collection.standards())
682+
from application.utils.ga_parity import ga_matrix_standard_names
683+
684+
standards = ga_matrix_standard_names(collection)
685+
rows = (
686+
collection.session.query(
687+
db.GapAnalysisResults.cache_key, db.GapAnalysisResults.ga_object
688+
)
689+
.filter(not_(db.GapAnalysisResults.cache_key.like("% >> %->%")))
690+
.all()
691+
)
674692
existing = {
675-
key
676-
for (key,) in collection.session.query(db.GapAnalysisResults.cache_key).all()
693+
str(key)
694+
for key, payload in rows
695+
if key
696+
and gap_analysis.primary_gap_analysis_payload_is_material(str(payload or ""))
677697
}
678698
missing: List[Tuple[str, str]] = []
679699
for sa in standards:

application/database/db.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from flask import json as flask_json
2222
from sqlalchemy.orm import aliased
2323
from flask_sqlalchemy.model import DefaultMeta
24-
from sqlalchemy import func, delete, cast as sql_cast, literal
24+
from sqlalchemy import func, delete, cast as sql_cast, literal, or_
2525
from sqlalchemy.dialects.postgresql import JSONB
2626
from sqlalchemy.exc import OperationalError, IntegrityError
2727

@@ -42,9 +42,11 @@
4242
from application.defs import cre_defs
4343
from application.utils import file
4444
from application.utils.gap_analysis import (
45+
gap_analysis_cache_key_is_primary,
4546
get_path_score,
4647
make_resources_key,
4748
make_subresources_key,
49+
primary_gap_analysis_payload_is_material,
4850
)
4951

5052

@@ -2300,10 +2302,16 @@ def add_embedding(
23002302
return existing
23012303

23022304
def gap_analysis_exists(self, cache_key) -> bool:
2303-
q = self.session.query(GapAnalysisResults).filter(
2304-
GapAnalysisResults.cache_key == cache_key
2305+
row = (
2306+
self.session.query(GapAnalysisResults)
2307+
.filter(GapAnalysisResults.cache_key == cache_key)
2308+
.first()
23052309
)
2306-
return self.session.query(q.exists()).scalar()
2310+
if row is None:
2311+
return False
2312+
if gap_analysis_cache_key_is_primary(cache_key):
2313+
return primary_gap_analysis_payload_is_material(row.ga_object)
2314+
return True
23072315

23082316
def get_gap_analysis_result(self, cache_key) -> str:
23092317
logger.info(f"looking for gap analysis with cache key: {cache_key}")
@@ -2318,7 +2326,16 @@ def get_gap_analysis_result(self, cache_key) -> str:
23182326
logger.info(f"did not find gap analysis with cache key: {cache_key}")
23192327

23202328
def add_gap_analysis_result(self, cache_key: str, ga_object: str):
2321-
if not self.gap_analysis_exists(cache_key):
2329+
existing = (
2330+
self.session.query(GapAnalysisResults)
2331+
.filter(GapAnalysisResults.cache_key == cache_key)
2332+
.first()
2333+
)
2334+
if existing:
2335+
existing.ga_object = ga_object
2336+
self.session.add(existing)
2337+
self.session.commit()
2338+
else:
23222339
logger.info(f"adding gap analysis result with cache key: {cache_key}")
23232340
res = GapAnalysisResults(cache_key=cache_key, ga_object=ga_object)
23242341
self.session.add(res)
@@ -2490,6 +2507,20 @@ def gap_analysis(
24902507
grouped_paths[key] = {"start": node, "paths": {}, "extra": 0}
24912508
extra_paths_dict[key] = {"paths": {}}
24922509

2510+
# Paths may start from CRE nodes that were not included in ``base_standard`` (or
2511+
# ``base_standard`` entries were skipped due to empty ids). Seed roots from each
2512+
# path's start so we never drop Neo paths or hit KeyError in the merge loop below.
2513+
for path in paths:
2514+
start_doc = path.get("start")
2515+
if start_doc is None:
2516+
continue
2517+
key = getattr(start_doc, "id", "") or ""
2518+
if not key:
2519+
continue
2520+
if key not in grouped_paths:
2521+
grouped_paths[key] = {"start": start_doc, "paths": {}, "extra": 0}
2522+
extra_paths_dict[key] = {"paths": {}}
2523+
24932524
for path in paths:
24942525
key = path["start"].id
24952526
end_key = path["end"].id
@@ -2525,6 +2556,30 @@ def gap_analysis(
25252556

25262557
if cache_key == "":
25272558
cache_key = make_resources_key(node_names)
2559+
if not grouped_paths:
2560+
if gap_analysis_cache_key_is_primary(cache_key):
2561+
stale = (
2562+
cre_db.session.query(GapAnalysisResults)
2563+
.filter(
2564+
or_(
2565+
GapAnalysisResults.cache_key == cache_key,
2566+
GapAnalysisResults.cache_key.like(cache_key + "->%"),
2567+
)
2568+
)
2569+
.all()
2570+
)
2571+
for row in stale:
2572+
cre_db.session.delete(row)
2573+
if stale:
2574+
cre_db.session.commit()
2575+
logger.warning(
2576+
"Not persisting gap analysis for %s: grouped_paths is empty "
2577+
"(Neo likely had no parseable base standard or no paths). "
2578+
"Re-run after Neo is populated so the pair is not locked in as an empty cache.",
2579+
cache_key,
2580+
)
2581+
return (node_names, grouped_paths, extra_paths_dict)
2582+
25282583
logger.info(f"got gap analysis paths for {'>>>'.join(node_names)}, storing result")
25292584
cre_db.add_gap_analysis_result(
25302585
cache_key=cache_key, ga_object=flask_json.dumps({"result": grouped_paths})

0 commit comments

Comments
 (0)