diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1db0587d..0dbaff31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # Ruff for Python linting and formatting - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.2 + rev: v0.16.8 hooks: # Run the linter - id: ruff diff --git a/docs/superpowers/plans/2026-07-17-knowledge-to-postgres.md b/docs/superpowers/plans/2026-07-17-knowledge-to-postgres.md index d24a8502..346b4139 100644 --- a/docs/superpowers/plans/2026-07-17-knowledge-to-postgres.md +++ b/docs/superpowers/plans/2026-07-17-knowledge-to-postgres.md @@ -162,8 +162,14 @@ In the `User` class (around line 56), add after `email_verified`: Ensure `Index` is imported from sqlalchemy: ```python from sqlalchemy import ( - Boolean, String, Integer, UniqueConstraint, ForeignKey, - Text, DateTime, Index, + Boolean, + String, + Integer, + UniqueConstraint, + ForeignKey, + Text, + DateTime, + Index, ) ``` @@ -172,11 +178,20 @@ from sqlalchemy import ( Add all new models to the imports and `__all__`: ```python from backend.app.pgdatabase.models import ( - Base, User, Conversation, QueryHistory, RecentConnection, - PasswordResetToken, OtpCode, - VerifiedQA, Glossary, - CatalogColumn, CatalogMetric, CatalogJoin, - CatalogSynonym, CatalogValueMapping, + Base, + User, + Conversation, + QueryHistory, + RecentConnection, + PasswordResetToken, + OtpCode, + VerifiedQA, + Glossary, + CatalogColumn, + CatalogMetric, + CatalogJoin, + CatalogSynonym, + CatalogValueMapping, ) ``` @@ -220,9 +235,13 @@ from difflib import SequenceMatcher from sqlalchemy import select, delete, func from backend.app.pgdatabase.models import ( - VerifiedQA, Glossary, - CatalogColumn, CatalogMetric, CatalogJoin, - CatalogSynonym, CatalogValueMapping, + VerifiedQA, + Glossary, + CatalogColumn, + CatalogMetric, + CatalogJoin, + CatalogSynonym, + CatalogValueMapping, ) from backend.app.utils import _tokens @@ -250,21 +269,32 @@ class KnowledgeService: tb = _tokens(question) b_lower = question.lower() for e in existing: - if _similarity(e["question"], question, tb, b_lower) > DUPLICATE_THRESHOLD: + if ( + _similarity(e["question"], question, tb, b_lower) + > DUPLICATE_THRESHOLD + ): return - session.add(VerifiedQA( - user_id=user_id, db_id=db_id, question=question, - sql=sql, restatement=restatement, created_at=time.time(), - )) + session.add( + VerifiedQA( + user_id=user_id, + db_id=db_id, + question=question, + sql=sql, + restatement=restatement, + created_at=time.time(), + ) + ) await session.commit() async def get_verified(self, user_id, db_id): async with self._session_factory() as session: result = await session.execute( - select(VerifiedQA).where( + select(VerifiedQA) + .where( VerifiedQA.user_id == user_id, VerifiedQA.db_id == db_id, - ).order_by(VerifiedQA.created_at.desc()) + ) + .order_by(VerifiedQA.created_at.desc()) ) return [ {"question": r.question, "sql": r.sql, "restatement": r.restatement} @@ -300,11 +330,15 @@ class KnowledgeService: ) ) for t in terms: - session.add(Glossary( - user_id=user_id, db_id=db_id, - term=t.get("term", ""), maps_to=t.get("maps_to", ""), - sql_hint=t.get("sql_hint", ""), - )) + session.add( + Glossary( + user_id=user_id, + db_id=db_id, + term=t.get("term", ""), + maps_to=t.get("maps_to", ""), + sql_hint=t.get("sql_hint", ""), + ) + ) await session.commit() async def get_glossary(self, user_id, db_id): @@ -320,11 +354,31 @@ class KnowledgeService: ] _CATALOG_CLASSES = { - "column_descriptions": (CatalogColumn, ("table_name", "column_name", "description"), ("table", "column", "description")), - "metrics": (CatalogMetric, ("name", "description", "sql_expression"), ("name", "description", "sql_expression")), - "joins": (CatalogJoin, ("tables", "join_condition", "description"), ("tables", "join_condition", "description")), - "synonyms": (CatalogSynonym, ("term", "entity_type", "entity_name"), ("term", "entity_type", "entity_name")), - "value_maps": (CatalogValueMapping, ("table_name", "column_name", "db_value", "business_label"), ("table", "column", "db_value", "business_label")), + "column_descriptions": ( + CatalogColumn, + ("table_name", "column_name", "description"), + ("table", "column", "description"), + ), + "metrics": ( + CatalogMetric, + ("name", "description", "sql_expression"), + ("name", "description", "sql_expression"), + ), + "joins": ( + CatalogJoin, + ("tables", "join_condition", "description"), + ("tables", "join_condition", "description"), + ), + "synonyms": ( + CatalogSynonym, + ("term", "entity_type", "entity_name"), + ("term", "entity_type", "entity_name"), + ), + "value_maps": ( + CatalogValueMapping, + ("table_name", "column_name", "db_value", "business_label"), + ("table", "column", "db_value", "business_label"), + ), } async def set_catalog(self, user_id, db_id, catalog): @@ -369,13 +423,25 @@ class KnowledgeService: async def trust_level(self, user_id, db_id): n = await self.count_verified(user_id, db_id) if n >= 7: - return {"level": "Trusted", "verified": n, "pct": 100, - "note": "Answers shown directly; reasoning on tap."} + return { + "level": "Trusted", + "verified": n, + "pct": 100, + "note": "Answers shown directly; reasoning on tap.", + } if n >= 3: - return {"level": "Assisted", "verified": n, "pct": 55, - "note": "Confident answers shown; novel ones get a second look."} - return {"level": "Supervised", "verified": n, "pct": max(8, n * 7), - "note": "Every answer waits for your confirmation while it learns."} + return { + "level": "Assisted", + "verified": n, + "pct": 55, + "note": "Confident answers shown; novel ones get a second look.", + } + return { + "level": "Supervised", + "verified": n, + "pct": max(8, n * 7), + "note": "Every answer waits for your confirmation while it learns.", + } ``` - [ ] **Step 2: Create `tests/test_knowledge_service.py`** @@ -478,9 +544,9 @@ async def test_set_and_get_glossary(kbs): kbs._session_factory.return_value.__aenter__.return_value = session session.execute.return_value.scalars.return_value.all.return_value = [] - await kbs.set_glossary(USER_ID, DB_ID, [ - {"term": "revenue", "maps_to": "orders.total", "sql_hint": ""} - ]) + await kbs.set_glossary( + USER_ID, DB_ID, [{"term": "revenue", "maps_to": "orders.total", "sql_hint": ""}] + ) session.add.assert_called_once() @@ -547,6 +613,7 @@ Add `"KnowledgeService"` to `__all__`. Replace: ```python from backend.app.knowledge import KnowledgeBase + ... kb = KnowledgeBase(cfgmod.KB_FILE) ... @@ -557,6 +624,7 @@ With: ```python from backend.app.pgdatabase import KnowledgeService from backend.app.pgdatabase.engine import async_session + ... kbs = KnowledgeService(async_session) ... @@ -623,7 +691,9 @@ After: result["trust"] = await kb.trust_level(user_id, db_id) result["glossary"] = await kb.get_glossary(user_id, db_id) result["has_knowledge"] = await kb.count_verified(user_id, db_id) > 0 -result["starters"] = [v["question"] for v in (await kb.get_verified(user_id, db_id))[:6]] +result["starters"] = [ + v["question"] for v in (await kb.get_verified(user_id, db_id))[:6] +] ``` The `connect_sample` function already has `user_id` as a parameter. Add `user_id` @@ -657,7 +727,13 @@ retrieved = await kb.retrieve_similar(user_id, db_id, q, k=3) In `feedback` (line 221): ```python -await kb.add_verified(user_id, db.get_db_id(user_id), req_data.question, req_data.sql, req_data.restatement) +await kb.add_verified( + user_id, + db.get_db_id(user_id), + req_data.question, + req_data.sql, + req_data.restatement, +) ``` And line 224: ```python @@ -670,7 +746,13 @@ v["question"] for v in (await kb.get_verified(user_id, db.get_db_id(user_id)))[: In `verify` (lines 235-238): ```python -await kb.add_verified(user_id, db.get_db_id(user_id), req_data.question, req_data.sql, req_data.restatement) +await kb.add_verified( + user_id, + db.get_db_id(user_id), + req_data.question, + req_data.sql, + req_data.restatement, +) return {"ok": True, "trust": await kb.trust_level(user_id, db.get_db_id(user_id))} ``` @@ -731,15 +813,16 @@ git commit -m "feat: update controllers for KnowledgeService (user_id + await)" async def get_state(user_id, db, cfg, kb, session_factory): s = {"connected": db.connected(user_id), "config": cfgmod.public_config(cfg)} if db.connected(user_id): - s["database"] = { - ... - } + s["database"] = {...} ... # Fetch tour_completed from user record from backend.app.pgdatabase.models import User from sqlalchemy import select + async with session_factory() as session: - result = await session.execute(select(User.tour_completed).where(User.id == user_id)) + result = await session.execute( + select(User.tour_completed).where(User.id == user_id) + ) s["tour_completed"] = result.scalar() or False return s ``` @@ -751,6 +834,7 @@ Note: `get_state` needs a `session_factory` param. Update its callers. ```python from backend.app.pgdatabase.engine import async_session + @router.post("/api/tour-complete") async def tour_complete( user_token=Depends(get_current_user), @@ -758,6 +842,7 @@ async def tour_complete( ): from backend.app.pgdatabase.models import User from sqlalchemy import update + uid = user_token["user_id"] async with async_session() as session: await session.execute( @@ -776,6 +861,7 @@ Actually, `async_session` is already importable from `pgdatabase.engine`: ```python from backend.app.pgdatabase.engine import async_session + @router.get("/api/state") async def get_state_route( user_token=Depends(get_current_user), diff --git a/docs/superpowers/plans/2026-07-17-openrouter-migration-plan.md b/docs/superpowers/plans/2026-07-17-openrouter-migration-plan.md index 196f14e9..3393a3ac 100644 --- a/docs/superpowers/plans/2026-07-17-openrouter-migration-plan.md +++ b/docs/superpowers/plans/2026-07-17-openrouter-migration-plan.md @@ -71,11 +71,14 @@ def test_complete_sends_correct_messages(mock_openai): mock_client = AsyncMock() mock_openai.return_value = mock_client mock_client.chat.completions.create.return_value = type( - "obj", (), - {"choices": [type("obj", (), - {"message": type("obj", (), - {"content": "Hello"})()})()], - "usage": None} + "obj", + (), + { + "choices": [ + type("obj", (), {"message": type("obj", (), {"content": "Hello"})()})() + ], + "usage": None, + }, )() p = OpenRouterProvider(api_key="sk-test") @@ -95,11 +98,14 @@ def test_complete_passes_json_schema(mock_openai): mock_client = AsyncMock() mock_openai.return_value = mock_client mock_client.chat.completions.create.return_value = type( - "obj", (), - {"choices": [type("obj", (), - {"message": type("obj", (), - {"content": "{}"})()})()], - "usage": None} + "obj", + (), + { + "choices": [ + type("obj", (), {"message": type("obj", (), {"content": "{}"})()})() + ], + "usage": None, + }, )() p = OpenRouterProvider(api_key="sk-test") @@ -115,6 +121,7 @@ def test_complete_raises_llm_error_on_api_error(mock_openai): mock_client = AsyncMock() mock_openai.return_value = mock_client from openai import APIError + mock_client.chat.completions.create.side_effect = APIError( message="Bad request", request=None, body=None ) @@ -129,11 +136,14 @@ def test_health_check_ok(mock_openai): mock_client = AsyncMock() mock_openai.return_value = mock_client mock_client.chat.completions.create.return_value = type( - "obj", (), - {"choices": [type("obj", (), - {"message": type("obj", (), - {"content": "ok"})()})()], - "usage": None} + "obj", + (), + { + "choices": [ + type("obj", (), {"message": type("obj", (), {"content": "ok"})()})() + ], + "usage": None, + }, )() p = OpenRouterProvider(api_key="sk-test") @@ -585,16 +595,12 @@ async def generate_sql( In `shortlist_tables`, remove `thinking_budget=0` from the `provider.complete()` call: ```python -raw = await provider.complete( - system, question, json_mode=True, schema=SHORTLIST_SCHEMA -) +raw = await provider.complete(system, question, json_mode=True, schema=SHORTLIST_SCHEMA) ``` In `explain_sql`, remove `thinking_budget=0`: ```python -raw = await provider.complete( - system, sql, json_mode=True, schema=EXPLAIN_SCHEMA -) +raw = await provider.complete(system, sql, json_mode=True, schema=EXPLAIN_SCHEMA) ``` In `suggest_catalog`, remove `thinking_budget=0`: @@ -737,11 +743,13 @@ def test_legacy_data_is_dropped(tmp_path): with _paths(tmp_path) as config_dir: config_dir.mkdir(exist_ok=True) (config_dir / "config.json").write_text( - json.dumps({ - "provider": "gemini", - "model": "gemini-flash-latest", - "api_keys": {"user-1": {"gemini": "AIza-old"}}, - }) + json.dumps( + { + "provider": "gemini", + "model": "gemini-flash-latest", + "api_keys": {"user-1": {"gemini": "AIza-old"}}, + } + ) ) cfg = load_config() assert "api_keys" not in cfg @@ -840,6 +848,7 @@ def _db_url_fernet(): CONFIG_DIR.mkdir(parents=True, exist_ok=True) import base64 import hashlib + if _DB_URL_KEY_FILE.exists(): secret = _DB_URL_KEY_FILE.read_text().strip() else: @@ -977,6 +986,7 @@ async def get_health(pg_status="unknown"): if supabase_url: try: import httpx + jwks_url = f"{supabase_url}/auth/v1/.well-known/jwks.json" async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(jwks_url) @@ -1143,6 +1153,7 @@ async def health(): pg_status = "connected" try: from backend.app.pgdatabase import get_engine + engine = get_engine() async with engine.connect() as conn: await conn.execute(text("SELECT 1")) diff --git a/docs/superpowers/plans/2026-07-17-post-migration-cleanup-plan.md b/docs/superpowers/plans/2026-07-17-post-migration-cleanup-plan.md index d049f4ef..cda7c5c2 100644 --- a/docs/superpowers/plans/2026-07-17-post-migration-cleanup-plan.md +++ b/docs/superpowers/plans/2026-07-17-post-migration-cleanup-plan.md @@ -332,7 +332,9 @@ to: ```python def test_save_config_writes_plain_dict(tmp_path): with _paths(tmp_path) as config_dir: - save_config({"openrouter_key": "sk-or-v1-secret", "last_db_url": "sqlite:///test.db"}) + save_config( + {"openrouter_key": "sk-or-v1-secret", "last_db_url": "sqlite:///test.db"} + ) saved = json.loads((config_dir / "config.json").read_text()) assert "openrouter_key" not in saved assert saved["last_db_url"] == "sqlite:///test.db" @@ -469,16 +471,18 @@ Do the same for the replacement block at line 1668. In `docs/superpowers/plans/2026-07-17-openrouter-migration-plan.md:979-981`, change: ```python - import httpx - jwks_url = f"{supabase_url}/auth/v1/.well-known/jwks.json" - resp = await httpx.AsyncClient(timeout=5).get(jwks_url) +import httpx + +jwks_url = f"{supabase_url}/auth/v1/.well-known/jwks.json" +resp = await httpx.AsyncClient(timeout=5).get(jwks_url) ``` to: ```python - import httpx - jwks_url = f"{supabase_url}/auth/v1/.well-known/jwks.json" - async with httpx.AsyncClient(timeout=5) as client: - resp = await client.get(jwks_url) +import httpx + +jwks_url = f"{supabase_url}/auth/v1/.well-known/jwks.json" +async with httpx.AsyncClient(timeout=5) as client: + resp = await client.get(jwks_url) ``` - [ ] **Step 4: Commit** diff --git a/docs/superpowers/specs/2026-07-17-knowledge-to-postgres-design.md b/docs/superpowers/specs/2026-07-17-knowledge-to-postgres-design.md index ec954141..062c59e6 100644 --- a/docs/superpowers/specs/2026-07-17-knowledge-to-postgres-design.md +++ b/docs/superpowers/specs/2026-07-17-knowledge-to-postgres-design.md @@ -105,6 +105,7 @@ so no frontend changes are needed. In `server.py`: ```python from backend.app.pgdatabase.knowledge import KnowledgeService + kbs = KnowledgeService(async_session) app.state.kbs = kbs ```