Kashish commited on
Commit
d35bd88
·
1 Parent(s): 7f32096

Added security constraints

Browse files
Files changed (4) hide show
  1. app.py +56 -17
  2. rag/chain.py +15 -19
  3. rag/prompts.py +4 -2
  4. rag/vectorstore.py +5 -2
app.py CHANGED
@@ -3,11 +3,15 @@ from __future__ import annotations
3
  from fastapi import FastAPI, HTTPException
4
  from fastapi.responses import RedirectResponse
5
  from pydantic import BaseModel, Field
6
- import re
 
 
7
 
8
  from rag.chain import aanswer
9
  from rag.retriever import get_retriever
10
 
 
 
11
  app = FastAPI(
12
  title="FAQ RAG Chatbot",
13
  version="1.0.0"
@@ -41,20 +45,55 @@ async def root() -> RedirectResponse:
41
 
42
  @app.post("/chat", response_model=ChatResponse)
43
  async def chat(payload: ChatRequest) -> ChatResponse:
 
 
44
  try:
45
- answer = await aanswer(payload.question)
46
- except Exception as error:
47
- raise HTTPException(status_code=500, detail=str(error))
48
-
49
- # Extract trailing "Sources:" block if present and return as separate list
50
- m = re.search(r"(?i)\bSources\s*:?\s*(.*)$", answer, flags=re.DOTALL)
51
- sources: list[str] = []
52
- if m:
53
- sources_text = m.group(1).strip()
54
- # Remove the Sources block from the answer text
55
- answer = answer[: m.start()].strip()
56
- # Split on commas and newlines, strip whitespace, ignore empties
57
- parts = re.split(r"[,\n;]+", sources_text)
58
- sources = [p.strip() for p in parts if p.strip()]
59
-
60
- return ChatResponse(question=payload.question, answer=answer, sources=sources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi import FastAPI, HTTPException
4
  from fastapi.responses import RedirectResponse
5
  from pydantic import BaseModel, Field
6
+ import logging
7
+
8
+ from google.genai.errors import ClientError, ServerError
9
 
10
  from rag.chain import aanswer
11
  from rag.retriever import get_retriever
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
  app = FastAPI(
16
  title="FAQ RAG Chatbot",
17
  version="1.0.0"
 
45
 
46
  @app.post("/chat", response_model=ChatResponse)
47
  async def chat(payload: ChatRequest) -> ChatResponse:
48
+ question = payload.question.strip()
49
+
50
  try:
51
+ response = await aanswer(question)
52
+ except ClientError as error:
53
+ message = str(error).lower()
54
+ if error.code == 429 or "quota" in message or "rate limit" in message:
55
+ raise HTTPException(
56
+ status_code=429,
57
+ detail=(
58
+ "The language model quota has been exceeded. "
59
+ "Please try again later."
60
+ ),
61
+ ) from None
62
+ raise HTTPException(
63
+ status_code=400,
64
+ detail=(
65
+ "Unable to process your request right now. "
66
+ "Please try again later."
67
+ ),
68
+ ) from None
69
+ except ServerError as error:
70
+ if error.code == 503 or "unavailable" in str(error).lower():
71
+ raise HTTPException(
72
+ status_code=503,
73
+ detail=(
74
+ "The language service is temporarily unavailable. "
75
+ "Please try again later."
76
+ ),
77
+ ) from None
78
+ raise HTTPException(
79
+ status_code=502,
80
+ detail=(
81
+ "Unable to process your request right now. "
82
+ "Please try again later."
83
+ ),
84
+ ) from None
85
+ except Exception:
86
+ logger.exception("Chat handler error")
87
+ raise HTTPException(
88
+ status_code=502,
89
+ detail=(
90
+ "Unable to process your request right now. "
91
+ "Please try again later."
92
+ ),
93
+ )
94
+
95
+ return ChatResponse(
96
+ question=question,
97
+ answer=response.answer,
98
+ sources=response.sources,
99
+ )
rag/chain.py CHANGED
@@ -3,25 +3,19 @@
3
  from functools import lru_cache
4
  from typing import Any
5
 
6
- from langchain_core.output_parsers import StrOutputParser
7
  from langchain_core.runnables import RunnableLambda, RunnablePassthrough
8
  from langchain_google_genai import ChatGoogleGenerativeAI
 
9
 
10
  from .config import settings
11
  from .prompts import prompt
12
  from .retriever import get_retriever
13
 
14
 
15
- def _clean_answer(text: str) -> str:
16
- compact = " ".join(text.split())
17
- source_prefix = "Sources:"
18
- index = compact.find(source_prefix)
19
- if index == -1:
20
- return compact
21
-
22
- before = compact[:index].rstrip()
23
- sources = compact[index:]
24
- return f"{before}\n{sources}"
25
 
26
 
27
  @lru_cache(maxsize=1)
@@ -41,20 +35,22 @@ def get_chain() -> Any:
41
  for i, doc in enumerate(docs)
42
  )
43
  )
 
 
 
 
44
 
45
  return (
46
  {"context": get_retriever() | context_formatter, "question": RunnablePassthrough()}
47
- | prompt
48
  | llm
49
- | StrOutputParser()
50
  )
51
 
52
 
53
- def answer(question: str) -> str:
54
- raw_answer = get_chain().invoke(question)
55
- return _clean_answer(raw_answer)
56
 
57
 
58
- async def aanswer(question: str) -> str:
59
- raw_answer = await get_chain().ainvoke(question)
60
- return _clean_answer(raw_answer)
 
3
  from functools import lru_cache
4
  from typing import Any
5
 
6
+ from langchain_core.output_parsers import PydanticOutputParser
7
  from langchain_core.runnables import RunnableLambda, RunnablePassthrough
8
  from langchain_google_genai import ChatGoogleGenerativeAI
9
+ from pydantic import BaseModel
10
 
11
  from .config import settings
12
  from .prompts import prompt
13
  from .retriever import get_retriever
14
 
15
 
16
+ class StructuredAnswer(BaseModel):
17
+ answer: str
18
+ sources: list[str]
 
 
 
 
 
 
 
19
 
20
 
21
  @lru_cache(maxsize=1)
 
35
  for i, doc in enumerate(docs)
36
  )
37
  )
38
+ output_parser = PydanticOutputParser(pydantic_object=StructuredAnswer)
39
+ prompt_with_format = prompt.bind(
40
+ format_instructions=output_parser.get_format_instructions()
41
+ )
42
 
43
  return (
44
  {"context": get_retriever() | context_formatter, "question": RunnablePassthrough()}
45
+ | prompt_with_format
46
  | llm
47
+ | output_parser
48
  )
49
 
50
 
51
+ def answer(question: str) -> StructuredAnswer:
52
+ return get_chain().invoke(question)
 
53
 
54
 
55
+ async def aanswer(question: str) -> StructuredAnswer:
56
+ return await get_chain().ainvoke(question)
 
rag/prompts.py CHANGED
@@ -8,11 +8,13 @@ prompt = ChatPromptTemplate.from_messages(
8
  "Use only the provided context. "
9
  "If the answer is not in the context, say you could not find it in the FAQ. "
10
  "Do not invent details. "
11
- "End with one line: Sources: <comma-separated ids>.",
 
 
12
  ),
13
  (
14
  "human",
15
- "Question: {question}\n\nContext:\n{context}\n\nAnswer in 3 to 5 sentences. Do not add filler or extra commentary.",
16
  ),
17
  ]
18
  )
 
8
  "Use only the provided context. "
9
  "If the answer is not in the context, say you could not find it in the FAQ. "
10
  "Do not invent details. "
11
+ "If the user asks for the full FAQ list, all FAQ entries, or a database dump --> reply with: 'I can only answer one specific FAQ question at a time. Please ask a single FAQ question.' "
12
+ "End with one line: Sources: <comma-separated ids>. "
13
+ "Do not add any extra commentary outside the JSON object."
14
  ),
15
  (
16
  "human",
17
+ "Question: {question}\n\nContext:\n{context}\n\n{format_instructions}",
18
  ),
19
  ]
20
  )
rag/vectorstore.py CHANGED
@@ -1,11 +1,14 @@
1
  from __future__ import annotations
2
 
 
3
  from typing import Optional, Iterable, List
4
  from langchain_core.embeddings import Embeddings
5
  from langchain_community.vectorstores import FAISS
6
  from .config import settings
7
  from .splitter import documents
8
 
 
 
9
  # Directory where FAISS index is persisted
10
  _VECTORSTORE_DIR = settings.vectorstore_dir
11
  _INDEX_NAME = settings.vectorstore_index_name
@@ -65,8 +68,8 @@ def get_vectorstore() -> FAISS:
65
  try:
66
  _vectorstore = FAISS.load_local(str(_VECTORSTORE_DIR), _embeddings, index_name=_INDEX_NAME)
67
  return _vectorstore
68
- except Exception:
69
- # Fall back to building the index (this will initialize the embedding model).
70
  _vectorstore = FAISS.from_documents(documents, _embeddings) # will call embed_documents
71
  _vectorstore.save_local(str(_VECTORSTORE_DIR), index_name=_INDEX_NAME)
72
  return _vectorstore
 
1
  from __future__ import annotations
2
 
3
+ import logging
4
  from typing import Optional, Iterable, List
5
  from langchain_core.embeddings import Embeddings
6
  from langchain_community.vectorstores import FAISS
7
  from .config import settings
8
  from .splitter import documents
9
 
10
+ logger = logging.getLogger(__name__)
11
+
12
  # Directory where FAISS index is persisted
13
  _VECTORSTORE_DIR = settings.vectorstore_dir
14
  _INDEX_NAME = settings.vectorstore_index_name
 
68
  try:
69
  _vectorstore = FAISS.load_local(str(_VECTORSTORE_DIR), _embeddings, index_name=_INDEX_NAME)
70
  return _vectorstore
71
+ except (FileNotFoundError, OSError) as error:
72
+ logger.info("FAISS index missing or unreadable; rebuilding index. %s", error)
73
  _vectorstore = FAISS.from_documents(documents, _embeddings) # will call embed_documents
74
  _vectorstore.save_local(str(_VECTORSTORE_DIR), index_name=_INDEX_NAME)
75
  return _vectorstore