Jaita commited on
Commit
36537c5
·
verified ·
1 Parent(s): 32a637a

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +211 -0
main.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ os.environ["POSTHOG_DISABLED"] = "true" # Disable PostHog telemetry
4
+ import requests
5
+ from fastapi import FastAPI, HTTPException, Request
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
8
+ from dotenv import load_dotenv
9
+ from kb_embed import collection, ingest_documents, search_knowledge_base, model
10
+ from contextlib import asynccontextmanager
11
+ from services.login import router as login_router
12
+ from services.generate_ticket import create_incident
13
+ from states import ChatState
14
+ from helpers import get_session, reset_session, get_user_only_text
15
+
16
+ # Load environment variables from the .env file
17
+ load_dotenv()
18
+
19
+ # --- 1. Initialize FastAPI ---
20
+ @asynccontextmanager
21
+ async def lifespan(app: FastAPI):
22
+ try:
23
+ folder_path = os.path.join(os.getcwd(), "documents")
24
+ ingest_documents(folder_path)
25
+ #if collection.count() == 0:
26
+ # print("🔍 KB empty. Running ingestion...")
27
+ # ingest_documents(folder_path)
28
+ #else:
29
+ # print(f"✅ KB already populated with {collection.count()} entries. Skipping ingestion.")
30
+ except Exception as e:
31
+ print(f"⚠️ KB ingestion failed: {e}")
32
+ yield
33
+
34
+ app = FastAPI(lifespan=lifespan)
35
+
36
+ # Mount the login routes
37
+ app.include_router(login_router)
38
+
39
+ # --- 2. Configure CORS ---
40
+ origins = [
41
+ "http://localhost:5173",
42
+ "http://localhost:3000",
43
+ ]
44
+
45
+ app.add_middleware(
46
+ CORSMiddleware,
47
+ allow_origins=origins,
48
+ allow_credentials=True,
49
+ allow_methods=["*"],
50
+ allow_headers=["*"],
51
+ )
52
+
53
+ # --- 3. Define the Request Data Structure ---
54
+ class ChatInput(BaseModel):
55
+ user_message: str
56
+ session_id: str
57
+
58
+ # NEW: Request model for incident creation (from frontend)
59
+ class IncidentInput(BaseModel):
60
+ short_description: str
61
+ description: str
62
+ # Optional: add username later if you decide to map caller_id
63
+ # username: Optional[str] = None
64
+
65
+ INC_FALLBACK_KEYWORD = "INC_FALLBACK"
66
+ MIN_SIMILARITY_THRESHOLD = 0.60
67
+ MAX_CLARIFICATIONS = 2
68
+
69
+ # --- 4. Gemini API Setup ---
70
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
71
+ GEMINI_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key={GEMINI_API_KEY}"
72
+
73
+ # --- 5. Endpoints ---
74
+ @app.get("/")
75
+ async def health_check():
76
+ return {"status": "ok"}
77
+
78
+
79
+ CLARIFY_QUESTIONS = [
80
+ "Can you share the exact error message you are seeing?",
81
+ "When did this issue start?",
82
+ "Is this happening for all users or only you?"
83
+ ]
84
+
85
+
86
+ @app.post("/chat")
87
+ async def chat_with_ai(input_data: ChatInput, request: Request):
88
+ try:
89
+ # =================================================
90
+ # Session Handling
91
+ # =================================================
92
+ session = get_session(input_data.session_id)
93
+ user_text = input_data.user_message.strip()
94
+ if "miss_count" not in session:
95
+ session["miss_count"] = 0
96
+ if "last_answered_docs" not in session:
97
+ session["last_answered_docs"] = []
98
+ text = user_text.lower()
99
+ action_words = ["raise","create","log","open"]
100
+ object_words = ["incident","ticket"]
101
+ has_action = any(w in text for w in action_words)
102
+ has_object = any(w in text for w in object_words)
103
+ if has_action and has_object:
104
+ return {"bot_response":"Please provide incident details.","state":"RAISE_INCIDENT"}
105
+
106
+ session["messages"].append({"role": "user", "text": user_text})
107
+
108
+ # Combine all user messages for better query context
109
+ combined_query = get_user_only_text(session["messages"])
110
+
111
+ #print("combined_query: ",combined_query)
112
+ # =================================================
113
+ # RAG: Knowledge Base Search
114
+ # =================================================
115
+
116
+
117
+ kb_results = search_knowledge_base(input_data.user_message, top_k=2)
118
+ print("kb_results: ",kb_results)
119
+ # Extract relevant context from search results
120
+ if not kb_results:
121
+ # --- FAILURE PATH ---
122
+ session["miss_count"] += 1
123
+ # Check if there was previous context
124
+ if session["last_answered_docs"]:
125
+ return {
126
+ "bot_response": "I didn’t find anything new, but I’m happy to clarify more about what we discussed earlier. Can you specify what you want to know next?",
127
+ "debug": f"Context found previously (Top {len(session['last_answered_docs'])} documents)",
128
+ "followup": "You can ask more questions or request an incident if needed."
129
+ }
130
+ if session["miss_count"] >= 2:
131
+ # Reset here so if they start a NEW topic after the ticket,
132
+ # they get another 2 chances.
133
+ session["miss_count"] = 0
134
+ return {"bot_response": "I couldn't find relevant SOP information. Please escalate to WMS Support or raise an incident.", "state": "SUGGEST_INCIDENT"}
135
+ else:
136
+ return {"bot_response": "I couldn't find a clear resolution in the SOP. Could you rephrase or add more detail?", "state": "REPHRASE"}
137
+
138
+
139
+ session["miss_count"] = 0 # Reset because we found something!
140
+ session["last_answered_docs"] = kb_results # store for conversational fallback
141
+ context = "\n\n".join(item["doc"] for item in kb_results if "doc" in item)
142
+
143
+ enhanced_prompt = f"""Use the following knowledge base context to answer the user's question accurately.
144
+ If the context contains relevant information, base your answer on it.
145
+ If the context doesn't help, say please raise an incident.
146
+ Knowledge Base Context:
147
+ {context}
148
+ User Question: {input_data.user_message}
149
+ Answer:"""
150
+ headers = {"Content-Type": "application/json"}
151
+ payload = {
152
+ "contents": [
153
+ {
154
+ "parts": [{"text": enhanced_prompt}]
155
+ }
156
+ ]
157
+ }
158
+ # =================================================
159
+ # Call Gemini API
160
+ # =================================================
161
+ response = requests.post(GEMINI_URL, headers=headers, json=payload, verify=False)
162
+ result = response.json()
163
+ # Extract Gemini's response safely
164
+ try:
165
+ bot_response = result["candidates"][0]["content"]["parts"][0]["text"].strip()
166
+ except Exception:
167
+ print("response.status_code: ",response.status_code,"\nresponse.text: ",response.text)
168
+ bot_response = "I encountered an error generating the response."
169
+ # =================================================
170
+ # Add Debug Info & Follow-up for Frontend
171
+ # =================================================
172
+ debug_info = f"Context found: {'Yes' if context else 'No'}"
173
+ if context:
174
+ debug_info += f" (Top {len(kb_results)} documents used)"
175
+ followup = (
176
+ "Hope this helps! Would you like me to raise an incident for tracking?"
177
+ if context else
178
+ "I couldn't find a resolution in the knowledge base. Should I raise an incident?"
179
+ )
180
+ return {
181
+ "bot_response": bot_response,
182
+ "debug": debug_info,
183
+ "followup":followup
184
+ }
185
+
186
+ except Exception as e:
187
+ raise HTTPException(status_code=500, detail=str(e))
188
+
189
+
190
+ @app.post("/incident")
191
+ async def raise_incident(input_data: IncidentInput):
192
+ """
193
+ Frontend calls this after the user says 'yes' and provides short & long description.
194
+ """
195
+ try:
196
+ result = create_incident(input_data.short_description, input_data.description)
197
+ print("result: ",result)
198
+ # Handle dict vs string returns
199
+ if isinstance(result, dict):
200
+ inc_number = result.get("number", "<unknown>")
201
+ sys_id = result.get("sys_id", "<unknown>")
202
+ ticket_text = f"Incident created: {inc_number}"
203
+ else:
204
+ ticket_text = str(result)
205
+
206
+ return {
207
+ "bot_response": f"✅ {ticket_text}\n\nIs there anything else I can assist you with?",
208
+ "debug": "Incident created via ServiceNow"
209
+ }
210
+ except Exception as e:
211
+ raise HTTPException(status_code=500, detail=str(e))