Vineetiitg commited on
Commit
73ee76f
·
1 Parent(s): a0010e2

feat: add production config and readiness checks

Browse files
.env.example ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PROJECT_NAME="Support Docs Copilot"
2
+ OLLAMA_BASE_URL=http://127.0.0.1:11434
3
+ OLLAMA_MODEL=llama3
4
+ QDRANT_URL=
5
+ QDRANT_LOCATION=./qdrant_data
6
+ COLLECTION_NAME=support_docs
7
+ RETRIEVAL_MODE=hybrid
8
+ RETRIEVAL_TOP_K=15
9
+ RERANKER_TOP_N=3
10
+ RERANKER_ENABLED=true
11
+ CHUNK_SIZE=500
12
+ CHUNK_OVERLAP=50
13
+ MIN_RELEVANCE_SCORE=0.0
14
+ MAX_CONTEXT_CHARS=12000
15
+ ENABLE_GUARDRAILS=true
16
+ ENABLE_RAG_EVAL=false
17
+ MAX_QUERY_LENGTH=2000
.gitignore CHANGED
@@ -15,7 +15,7 @@ frontend.log
15
  backend.log
16
  fastembed_cache/
17
  .cache/
18
- model files
19
- Hugging Face cache
20
- Ollama models
21
 
 
15
  backend.log
16
  fastembed_cache/
17
  .cache/
18
+ reports/*.html
19
+ reports/*.json
20
+ data/document_registry.json
21
 
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/core/config.py CHANGED
@@ -1,16 +1,15 @@
1
- import os
2
  from pydantic_settings import BaseSettings
3
 
4
  class Settings(BaseSettings):
5
  PROJECT_NAME: str = "Support Docs Copilot"
6
 
7
  # Ollama LLM Config
8
- OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://ollama:11434")
9
- OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "llama3")
10
 
11
  # Qdrant Vector DB Config
12
- QDRANT_URL: str = os.getenv("QDRANT_URL", "")
13
- QDRANT_LOCATION: str = os.getenv("QDRANT_LOCATION", "./qdrant_data")
14
  COLLECTION_NAME: str = "support_docs"
15
 
16
  # Embeddings Config
@@ -18,4 +17,25 @@ class Settings(BaseSettings):
18
  SPARSE_EMBEDDING_MODEL: str = "Qdrant/bm25"
19
  RERANKER_MODEL: str = "BAAI/bge-reranker-base"
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  settings = Settings()
 
 
1
  from pydantic_settings import BaseSettings
2
 
3
  class Settings(BaseSettings):
4
  PROJECT_NAME: str = "Support Docs Copilot"
5
 
6
  # Ollama LLM Config
7
+ OLLAMA_BASE_URL: str = "http://127.0.0.1:11434"
8
+ OLLAMA_MODEL: str = "llama3"
9
 
10
  # Qdrant Vector DB Config
11
+ QDRANT_URL: str = ""
12
+ QDRANT_LOCATION: str = "./qdrant_data"
13
  COLLECTION_NAME: str = "support_docs"
14
 
15
  # Embeddings Config
 
17
  SPARSE_EMBEDDING_MODEL: str = "Qdrant/bm25"
18
  RERANKER_MODEL: str = "BAAI/bge-reranker-base"
19
 
20
+ # Retrieval Config
21
+ RETRIEVAL_MODE: str = "hybrid"
22
+ RETRIEVAL_TOP_K: int = 15
23
+ RERANKER_TOP_N: int = 3
24
+ RERANKER_ENABLED: bool = True
25
+ MIN_RELEVANCE_SCORE: float = 0.0
26
+ MAX_CONTEXT_CHARS: int = 12000
27
+
28
+ # Chunking Config
29
+ CHUNK_SIZE: int = 500
30
+ CHUNK_OVERLAP: int = 50
31
+
32
+ # Runtime Safety
33
+ ENABLE_GUARDRAILS: bool = True
34
+ ENABLE_RAG_EVAL: bool = False
35
+ MAX_QUERY_LENGTH: int = 2000
36
+
37
+ class Config:
38
+ env_file = ".env"
39
+ extra = "ignore"
40
+
41
  settings = Settings()
app/core/dependencies.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ import requests
4
+ from qdrant_client import QdrantClient
5
+
6
+ from app.core.config import settings
7
+
8
+
9
+ def get_qdrant_client() -> QdrantClient:
10
+ if settings.QDRANT_URL:
11
+ return QdrantClient(url=settings.QDRANT_URL)
12
+ return QdrantClient(path=settings.QDRANT_LOCATION)
13
+
14
+
15
+ def check_ollama() -> dict[str, Any]:
16
+ try:
17
+ response = requests.get(f"{settings.OLLAMA_BASE_URL}/api/tags", timeout=3)
18
+ return {"ok": response.ok, "status_code": response.status_code}
19
+ except requests.RequestException as exc:
20
+ return {"ok": False, "error": str(exc)}
21
+
22
+
23
+ def check_qdrant() -> dict[str, Any]:
24
+ try:
25
+ client = get_qdrant_client()
26
+ collections = client.get_collections()
27
+ names = [collection.name for collection in collections.collections]
28
+ return {
29
+ "ok": settings.COLLECTION_NAME in names,
30
+ "collection": settings.COLLECTION_NAME,
31
+ "available_collections": names,
32
+ }
33
+ except Exception as exc:
34
+ return {"ok": False, "error": str(exc)}
app/core/errors.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ class CopilotError(Exception):
2
+ """Base application error with a user-safe message."""
3
+
4
+ def __init__(self, message: str, status_code: int = 500):
5
+ self.message = message
6
+ self.status_code = status_code
7
+ super().__init__(message)
app/core/logging.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+
4
+
5
+ def configure_logging() -> None:
6
+ logging.basicConfig(
7
+ level=logging.INFO,
8
+ format="%(asctime)s %(levelname)s %(name)s %(message)s",
9
+ handlers=[logging.StreamHandler(sys.stdout)],
10
+ force=True,
11
+ )
12
+
13
+
14
+ logger = logging.getLogger("support_docs_copilot")
app/engine/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/graph/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/guardrails/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/main.py CHANGED
@@ -1,4 +1,5 @@
1
  import asyncio
 
2
  from fastapi import FastAPI, HTTPException
3
  from fastapi.responses import StreamingResponse
4
  from pydantic import BaseModel
@@ -7,9 +8,12 @@ from langchain_core.prompts import PromptTemplate
7
  from langchain_ollama import ChatOllama
8
 
9
  from app.core.config import settings
 
 
10
  from app.graph.workflow import compile_workflow
11
  from app.guardrails.validators import DetectPromptInjection
12
 
 
13
  app = FastAPI(title=settings.PROJECT_NAME)
14
  rag_agent = compile_workflow()
15
  input_guard = Guard().use(DetectPromptInjection, on_fail="exception")
@@ -21,12 +25,28 @@ class ChatResponse(BaseModel):
21
  query: str
22
  answer: str
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  @app.post("/chat", response_model=ChatResponse)
25
  async def chat_endpoint(request: ChatRequest):
26
- try:
27
- input_guard.validate(request.query)
28
- except Exception as e:
29
- raise HTTPException(status_code=400, detail=str(getattr(e, "message", e)))
 
 
30
 
31
  initial_state = {"question": request.query, "run_count": 0}
32
  try:
@@ -35,16 +55,19 @@ async def chat_endpoint(request: ChatRequest):
35
  except Exception as e:
36
  raise HTTPException(status_code=500, detail=str(e))
37
 
 
38
  return ChatResponse(query=request.query, answer=answer)
39
 
40
  @app.post("/chat/stream")
41
  async def chat_stream_endpoint(request: ChatRequest):
42
- try:
43
- input_guard.validate(request.query)
44
- except Exception as e:
45
- raise HTTPException(status_code=400, detail=str(getattr(e, "message", e)))
 
46
 
47
  async def token_generator():
 
48
  initial_state = {"question": request.query, "run_count": 0}
49
  final_state = rag_agent.invoke(initial_state)
50
  documents = final_state.get("documents", [])
@@ -67,5 +90,6 @@ async def chat_stream_endpoint(request: ChatRequest):
67
  if chunk.content:
68
  yield chunk.content
69
  await asyncio.sleep(0.01)
 
70
 
71
  return StreamingResponse(token_generator(), media_type="text/event-stream")
 
1
  import asyncio
2
+ import time
3
  from fastapi import FastAPI, HTTPException
4
  from fastapi.responses import StreamingResponse
5
  from pydantic import BaseModel
 
8
  from langchain_ollama import ChatOllama
9
 
10
  from app.core.config import settings
11
+ from app.core.dependencies import check_ollama, check_qdrant
12
+ from app.core.logging import configure_logging, logger
13
  from app.graph.workflow import compile_workflow
14
  from app.guardrails.validators import DetectPromptInjection
15
 
16
+ configure_logging()
17
  app = FastAPI(title=settings.PROJECT_NAME)
18
  rag_agent = compile_workflow()
19
  input_guard = Guard().use(DetectPromptInjection, on_fail="exception")
 
25
  query: str
26
  answer: str
27
 
28
+ @app.get("/health")
29
+ async def health_endpoint():
30
+ return {"status": "ok", "project": settings.PROJECT_NAME}
31
+
32
+ @app.get("/ready")
33
+ async def ready_endpoint():
34
+ ollama = check_ollama()
35
+ qdrant = check_qdrant()
36
+ return {
37
+ "ready": bool(ollama.get("ok") and qdrant.get("ok")),
38
+ "ollama": ollama,
39
+ "qdrant": qdrant,
40
+ }
41
+
42
  @app.post("/chat", response_model=ChatResponse)
43
  async def chat_endpoint(request: ChatRequest):
44
+ started_at = time.perf_counter()
45
+ if settings.ENABLE_GUARDRAILS:
46
+ try:
47
+ input_guard.validate(request.query)
48
+ except Exception as e:
49
+ raise HTTPException(status_code=400, detail=str(getattr(e, "message", e)))
50
 
51
  initial_state = {"question": request.query, "run_count": 0}
52
  try:
 
55
  except Exception as e:
56
  raise HTTPException(status_code=500, detail=str(e))
57
 
58
+ logger.info("chat completed latency_ms=%s", round((time.perf_counter() - started_at) * 1000, 2))
59
  return ChatResponse(query=request.query, answer=answer)
60
 
61
  @app.post("/chat/stream")
62
  async def chat_stream_endpoint(request: ChatRequest):
63
+ if settings.ENABLE_GUARDRAILS:
64
+ try:
65
+ input_guard.validate(request.query)
66
+ except Exception as e:
67
+ raise HTTPException(status_code=400, detail=str(getattr(e, "message", e)))
68
 
69
  async def token_generator():
70
+ started_at = time.perf_counter()
71
  initial_state = {"question": request.query, "run_count": 0}
72
  final_state = rag_agent.invoke(initial_state)
73
  documents = final_state.get("documents", [])
 
90
  if chunk.content:
91
  yield chunk.content
92
  await asyncio.sleep(0.01)
93
+ logger.info("stream completed latency_ms=%s", round((time.perf_counter() - started_at) * 1000, 2))
94
 
95
  return StreamingResponse(token_generator(), media_type="text/event-stream")
app/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
ui/app.py CHANGED
@@ -1,9 +1,11 @@
1
- import streamlit as st
2
- import requests
3
  import os
4
 
5
- st.set_page_config(page_title="Support Docs Copilot", page_icon="🤖")
6
- st.title("🤖 Support Docs Copilot")
 
 
 
 
7
 
8
  BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000/chat/stream")
9
 
@@ -23,21 +25,21 @@ if user_query := st.chat_input("Ask a support question..."):
23
  response_placeholder = st.empty()
24
  full_response = ""
25
  try:
26
- with requests.post(BACKEND_URL, json={"query": user_query}, stream=True) as response:
27
  if response.status_code == 200:
28
  for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
29
  if chunk:
30
  full_response += chunk
31
- response_placeholder.markdown(full_response + "")
32
  response_placeholder.markdown(full_response)
33
  elif response.status_code == 400:
34
- full_response = f"⚠️ {response.json().get('detail', 'Violation.')}"
35
  response_placeholder.error(full_response)
36
  else:
37
- full_response = "⚠️ Server communication error."
38
  response_placeholder.error(full_response)
39
- except:
40
- full_response = "Backend connection error."
41
  response_placeholder.error(full_response)
42
 
43
  st.session_state.messages.append({"role": "assistant", "content": full_response})
 
 
 
1
  import os
2
 
3
+ import requests
4
+ import streamlit as st
5
+
6
+
7
+ st.set_page_config(page_title="Support Docs Copilot", page_icon="SD")
8
+ st.title("Support Docs Copilot")
9
 
10
  BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000/chat/stream")
11
 
 
25
  response_placeholder = st.empty()
26
  full_response = ""
27
  try:
28
+ with requests.post(BACKEND_URL, json={"query": user_query}, stream=True, timeout=120) as response:
29
  if response.status_code == 200:
30
  for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
31
  if chunk:
32
  full_response += chunk
33
+ response_placeholder.markdown(full_response + "...")
34
  response_placeholder.markdown(full_response)
35
  elif response.status_code == 400:
36
+ full_response = f"Warning: {response.json().get('detail', 'Violation.')}"
37
  response_placeholder.error(full_response)
38
  else:
39
+ full_response = "Warning: server communication error."
40
  response_placeholder.error(full_response)
41
+ except requests.RequestException:
42
+ full_response = "Backend connection error."
43
  response_placeholder.error(full_response)
44
 
45
  st.session_state.messages.append({"role": "assistant", "content": full_response})