From c13c66b2fbad15719805dd67169a30f11b35c88e Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Sat, 4 Jul 2026 12:50:43 +0800 Subject: [PATCH 1/9] Chore: Transfer lifespan to a different file --- backend/app/core/lifespan.py | 11 +++++++++++ backend/main.py | 9 +-------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 backend/app/core/lifespan.py diff --git a/backend/app/core/lifespan.py b/backend/app/core/lifespan.py new file mode 100644 index 0000000..1888e0d --- /dev/null +++ b/backend/app/core/lifespan.py @@ -0,0 +1,11 @@ +from .db import init_db +from contextlib import asynccontextmanager +from fastapi import FastAPI +from app.core.logging import logger + + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + logger.info("Database initialized") + yield \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 94f240a..f2e4d1c 100755 --- a/backend/main.py +++ b/backend/main.py @@ -1,19 +1,12 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.core.db import init_db from app.core.config import settings from app.core.rate_limit import setup_rate_limiting from app.api import ingest, manifest, validate, activity -from app.core.logging import logger +from app.core.lifespan import lifespan -from contextlib import asynccontextmanager -@asynccontextmanager -async def lifespan(app: FastAPI): - init_db() - logger.info("Database initialized") - yield app = FastAPI(title=settings.PROJECT_NAME, lifespan=lifespan) From 56bd7a3720ed2360b13f21e208218f1bdd0a68fa Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Sat, 4 Jul 2026 13:24:15 +0800 Subject: [PATCH 2/9] Feat: Open client connection with proper close connection socket for Groq api inference. Added app_state to hold those object. --- backend/app/core/lifespan.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/app/core/lifespan.py b/backend/app/core/lifespan.py index 1888e0d..748dba3 100644 --- a/backend/app/core/lifespan.py +++ b/backend/app/core/lifespan.py @@ -1,11 +1,31 @@ -from .db import init_db +from typing import Optional from contextlib import asynccontextmanager from fastapi import FastAPI from app.core.logging import logger +from openai import AsyncOpenAI + +from .db import init_db +from .config import settings +class AppState: + groq_client: Optional[AsyncOpenAI] = None + +app_state = AppState() + + @asynccontextmanager async def lifespan(app: FastAPI): - init_db() - logger.info("Database initialized") - yield \ No newline at end of file + + try: + init_db() + logger.info("Database initialized") + + app_state.groq_client = AsyncOpenAI( + base_url=settings.LLM_ENDPOINT, api_key=settings.LLM_API_KEY or "missing-key" + ) + yield + + finally: + + await app_state.groq_client.close() \ No newline at end of file From 3bfaed96a2c21e1c608daf1b80150271473ba530 Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Sat, 4 Jul 2026 13:27:57 +0800 Subject: [PATCH 3/9] Chore: Remove Client and use app_state instead --- backend/app/regintel/rag.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py index 4892a47..e57918b 100755 --- a/backend/app/regintel/rag.py +++ b/backend/app/regintel/rag.py @@ -6,8 +6,8 @@ from llama_index.embeddings.cohere import CohereEmbedding import sqlalchemy from app.core.config import settings -from openai import AsyncOpenAI from pathlib import Path +from ..core.lifespan import app_state # Detect project root (where .env lives) @@ -15,11 +15,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent -@lru_cache() -def get_openai_client(): - return AsyncOpenAI( - base_url=settings.LLM_ENDPOINT, api_key=settings.LLM_API_KEY or "missing-key" - ) + class VectorStoreConnection: @@ -88,8 +84,7 @@ async def vector_chat_async(self, query: str): {query} """ print(user_prompt) - - completion = await get_openai_client().chat.completions.create( + completion = await app_state.groq_client.chat.completions.create( model=settings.CHAT_MODEL, messages=[ {"role": "system", "content": system_prompt}, From 08310940ede9f0baef44d5341cc9563030142304 Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Sat, 4 Jul 2026 13:30:22 +0800 Subject: [PATCH 4/9] Feat: Added pgvector client connection and closure socket connection in lifespan.py --- backend/app/core/lifespan.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/app/core/lifespan.py b/backend/app/core/lifespan.py index 748dba3..2cc600d 100644 --- a/backend/app/core/lifespan.py +++ b/backend/app/core/lifespan.py @@ -3,11 +3,17 @@ from fastapi import FastAPI from app.core.logging import logger from openai import AsyncOpenAI +from llama_index.vector_stores.postgres import PGVectorStore +import sqlalchemy from .db import init_db from .config import settings + + + class AppState: groq_client: Optional[AsyncOpenAI] = None + vector_client: Optional[PGVectorStore] = None app_state = AppState() @@ -24,8 +30,21 @@ async def lifespan(app: FastAPI): app_state.groq_client = AsyncOpenAI( base_url=settings.LLM_ENDPOINT, api_key=settings.LLM_API_KEY or "missing-key" ) + + url = sqlalchemy.make_url(settings.DATABASE_URL) + app_state.vector_client = PGVectorStore.from_params( + host=url.host, + port=str(url.port or 5432), + user=url.username, + password=url.password, + database=url.database, + table_name="regulations_vectors", + embed_dim=1024, + ) yield finally: - - await app_state.groq_client.close() \ No newline at end of file + if app_state.groq_client: + await app_state.groq_client.close() + if app_state.vector_client: + await app_state.vector_client.close() From 2f13bd16f963b07accc00bad99491ea023d08b2c Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Sat, 4 Jul 2026 13:36:12 +0800 Subject: [PATCH 5/9] Chore: Remove the pgvectorstore and uses app_state from lifespan.py for a ready pgvector client connection --- backend/app/regintel/rag.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py index e57918b..1d87ea6 100755 --- a/backend/app/regintel/rag.py +++ b/backend/app/regintel/rag.py @@ -1,10 +1,8 @@ -from functools import lru_cache from llama_index.core import SimpleDirectoryReader, VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core.schema import NodeWithScore from llama_index.vector_stores.postgres import PGVectorStore from llama_index.embeddings.cohere import CohereEmbedding -import sqlalchemy from app.core.config import settings from pathlib import Path from ..core.lifespan import app_state @@ -25,17 +23,8 @@ def __init__(self): self.should_reset = False @property - def vector_store(self): - url = sqlalchemy.make_url(settings.DATABASE_URL) - return PGVectorStore.from_params( - host=url.host, - port=str(url.port or 5432), - user=url.username, - password=url.password, - database=url.database, - table_name="regulations_vectors", - embed_dim=1024, - ) + def vector_store(self) -> PGVectorStore: + return app_state.vector_client @property def embedding_model(self): From c1841628cd4231926b8cd6dac803e40e9936ca3e Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Thu, 6 Aug 2026 22:32:36 +0800 Subject: [PATCH 6/9] fix: ruff formatting and remove executable bit from main.py --- backend/app/core/lifespan.py | 16 ++++++++-------- backend/app/regintel/rag.py | 8 +++++--- backend/main.py | 9 +++------ 3 files changed, 16 insertions(+), 17 deletions(-) mode change 100755 => 100644 backend/main.py diff --git a/backend/app/core/lifespan.py b/backend/app/core/lifespan.py index 2cc600d..77c2478 100644 --- a/backend/app/core/lifespan.py +++ b/backend/app/core/lifespan.py @@ -1,19 +1,19 @@ -from typing import Optional from contextlib import asynccontextmanager + +import sqlalchemy from fastapi import FastAPI -from app.core.logging import logger -from openai import AsyncOpenAI from llama_index.vector_stores.postgres import PGVectorStore -import sqlalchemy +from openai import AsyncOpenAI -from .db import init_db -from .config import settings +from app.core.logging import logger +from .config import settings +from .db import init_db class AppState: - groq_client: Optional[AsyncOpenAI] = None - vector_client: Optional[PGVectorStore] = None + groq_client: AsyncOpenAI | None = None + vector_client: PGVectorStore | None = None app_state = AppState() diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py index a8a63d0..74a4ad7 100755 --- a/backend/app/regintel/rag.py +++ b/backend/app/regintel/rag.py @@ -1,12 +1,14 @@ +from pathlib import Path + from llama_index.core import SimpleDirectoryReader, VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core.schema import NodeWithScore -from llama_index.vector_stores.postgres import PGVectorStore from llama_index.embeddings.cohere import CohereEmbedding +from llama_index.vector_stores.postgres import PGVectorStore + from app.core.config import settings -from pathlib import Path -from ..core.lifespan import app_state +from ..core.lifespan import app_state # Detect project root (where .env lives) # rag.py is in backend/app/regintel/ diff --git a/backend/main.py b/backend/main.py old mode 100755 new mode 100644 index b561ab2..b9b44d3 --- a/backend/main.py +++ b/backend/main.py @@ -1,13 +1,10 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware + +from app.api import activity, ingest, manifest, validate from app.core.config import settings -from app.core.rate_limit import setup_rate_limiting -from app.api import ingest, manifest, validate, activity from app.core.lifespan import lifespan - - - - +from app.core.rate_limit import setup_rate_limiting app = FastAPI(title=settings.PROJECT_NAME, lifespan=lifespan) From dc9d15843c8971ab037319cd61747ee67a37e2d6 Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Thu, 6 Aug 2026 22:37:49 +0800 Subject: [PATCH 7/9] fix: remove executable bit from rag.py --- backend/app/regintel/rag.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 backend/app/regintel/rag.py diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py old mode 100755 new mode 100644 From 23c5aa159afa0024d6e2203765136de4d2bd3a85 Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Thu, 6 Aug 2026 22:48:44 +0800 Subject: [PATCH 8/9] style: format python code with ruff --- backend/app/core/lifespan.py | 6 +++--- backend/app/regintel/rag.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/app/core/lifespan.py b/backend/app/core/lifespan.py index 77c2478..7f3df4b 100644 --- a/backend/app/core/lifespan.py +++ b/backend/app/core/lifespan.py @@ -15,9 +15,8 @@ class AppState: groq_client: AsyncOpenAI | None = None vector_client: PGVectorStore | None = None -app_state = AppState() - +app_state = AppState() @asynccontextmanager @@ -28,7 +27,8 @@ async def lifespan(app: FastAPI): logger.info("Database initialized") app_state.groq_client = AsyncOpenAI( - base_url=settings.LLM_ENDPOINT, api_key=settings.LLM_API_KEY or "missing-key" + base_url=settings.LLM_ENDPOINT, + api_key=settings.LLM_API_KEY or "missing-key", ) url = sqlalchemy.make_url(settings.DATABASE_URL) diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py index 74a4ad7..fad045f 100644 --- a/backend/app/regintel/rag.py +++ b/backend/app/regintel/rag.py @@ -15,9 +15,6 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent - - - class VectorStoreConnection: def __init__(self): self.splitter = SentenceSplitter(chunk_size=512, chunk_overlap=60) From c3c03158c94baeb1a647cb70a93ed50ed502af7c Mon Sep 17 00:00:00 2001 From: Nana Mein Date: Thu, 6 Aug 2026 22:59:16 +0800 Subject: [PATCH 9/9] fix(rag): add assertions to satisfy type checker for optional clients --- backend/app/regintel/rag.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/app/regintel/rag.py b/backend/app/regintel/rag.py index fad045f..cf1f249 100644 --- a/backend/app/regintel/rag.py +++ b/backend/app/regintel/rag.py @@ -23,6 +23,7 @@ def __init__(self): @property def vector_store(self) -> PGVectorStore: + assert app_state.vector_client is not None, "Vector client is not initialized" return app_state.vector_client @property @@ -72,6 +73,7 @@ async def vector_chat_async(self, query: str): {query} """ print(user_prompt) + assert app_state.groq_client is not None, "Groq client is not initialized" completion = await app_state.groq_client.chat.completions.create( model=settings.CHAT_MODEL, messages=[