Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -12,65 +12,21 @@ from pydantic import BaseModel
|
|
| 12 |
from dotenv import load_dotenv
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
-
# ------------------------------
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
from services.kb_creation import bm25_docs
|
| 31 |
-
from services.login import router as login_router
|
| 32 |
-
from services.generate_ticket import get_valid_token, create_incident
|
| 33 |
-
except Exception:
|
| 34 |
-
# Lightweight fallbacks (only to make Pylance happy; you should use real implementations at runtime)
|
| 35 |
-
ACTION_SYNONYMS: Dict[str, List[str]] = {"create": ["create"], "update": ["update"], "delete": ["delete"]}
|
| 36 |
-
MODULE_VOCAB: Dict[str, List[str]] = {"appointments": ["appointment", "schedule"]}
|
| 37 |
-
bm25_docs: List[Dict[str, Any]] = []
|
| 38 |
-
|
| 39 |
-
class _DummyColl:
|
| 40 |
-
def count(self) -> int:
|
| 41 |
-
return 0
|
| 42 |
-
|
| 43 |
-
def collection() -> _DummyColl:
|
| 44 |
-
return _DummyColl()
|
| 45 |
-
|
| 46 |
-
def ingest_documents(folder_path: str) -> None:
|
| 47 |
-
raise NotImplementedError("ingest_documents stub in editor mode")
|
| 48 |
-
|
| 49 |
-
def hybrid_search_knowledge_base(q: str, top_k: int = 10, alpha: float = 0.6, beta: float = 0.4) -> Dict[str, Any]:
|
| 50 |
-
return {"documents": [], "metadatas": [], "distances": [], "combined_scores": [], "user_intent": "neutral", "best_doc": None, "actions": []}
|
| 51 |
-
|
| 52 |
-
def get_section_text(doc: Optional[str], section: Optional[str]) -> str:
|
| 53 |
-
return ""
|
| 54 |
-
|
| 55 |
-
def get_best_steps_section_text(doc: Optional[str]) -> str:
|
| 56 |
-
return ""
|
| 57 |
-
|
| 58 |
-
def get_best_errors_section_text(doc: Optional[str]) -> str:
|
| 59 |
-
return ""
|
| 60 |
-
|
| 61 |
-
def get_escalation_text(doc: Optional[str]) -> str:
|
| 62 |
-
return ""
|
| 63 |
-
|
| 64 |
-
class _DummyRouter:
|
| 65 |
-
pass
|
| 66 |
-
|
| 67 |
-
login_router = _DummyRouter()
|
| 68 |
-
|
| 69 |
-
def get_valid_token() -> str:
|
| 70 |
-
return ""
|
| 71 |
-
|
| 72 |
-
def create_incident(short: str, desc: str) -> Dict[str, Any]:
|
| 73 |
-
return {"error": "ServiceNow not configured"}
|
| 74 |
|
| 75 |
# ------------------------------ Config ------------------------------
|
| 76 |
VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
|
@@ -103,10 +59,7 @@ async def lifespan(app: FastAPI):
|
|
| 103 |
|
| 104 |
|
| 105 |
app = FastAPI(lifespan=lifespan)
|
| 106 |
-
|
| 107 |
-
app.include_router(login_router)
|
| 108 |
-
except Exception:
|
| 109 |
-
pass # stubbed router in editor-only mode
|
| 110 |
|
| 111 |
origins = ["https://chatbotnova-chatbot-frontend.hf.space"]
|
| 112 |
app.add_middleware(
|
|
@@ -221,6 +174,7 @@ def _detect_error_families(msg: str) -> list:
|
|
| 221 |
return fams
|
| 222 |
|
| 223 |
|
|
|
|
| 224 |
def _get_steps_by_title_keywords_global(keywords: List[str], prefer_doc: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 225 |
"""
|
| 226 |
Search ALL SOP docs for a 'steps' section whose *section title* contains any of `keywords`.
|
|
@@ -261,6 +215,7 @@ def _get_steps_by_title_keywords_global(keywords: List[str], prefer_doc: Optiona
|
|
| 261 |
_, best_doc_g, best_sec_g, best_text_g = candidates[0]
|
| 262 |
return best_doc_g, best_sec_g, best_text_g
|
| 263 |
|
|
|
|
| 264 |
# --- Action-targeted steps selector (uses existing KB metadata) ---
|
| 265 |
def _get_steps_for_action(best_doc: str, actions: list) -> Optional[str]:
|
| 266 |
"""
|
|
@@ -324,6 +279,7 @@ def _get_steps_for_action_global(actions: list, prefer_doc: Optional[str] = None
|
|
| 324 |
_, best_doc_global, best_text = candidates[0]
|
| 325 |
return best_doc_global, best_text
|
| 326 |
|
|
|
|
| 327 |
# --- Default section picker when query doesn't reveal action ---
|
| 328 |
def _pick_default_action_section(best_doc: str) -> Optional[str]:
|
| 329 |
"""
|
|
@@ -1334,7 +1290,7 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1334 |
next_step_applied = False
|
| 1335 |
next_step_info: Dict[str, Any] = {}
|
| 1336 |
|
| 1337 |
-
# ---------------- Steps branch
|
| 1338 |
if best_doc and detected_intent == "steps":
|
| 1339 |
context_preformatted = False
|
| 1340 |
full_steps = None
|
|
@@ -1480,7 +1436,7 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1480 |
### Output
|
| 1481 |
Return ONLY the rewritten guidance."""
|
| 1482 |
headers = {"Content-Type": "application/json"}
|
| 1483 |
-
payload = {"contents": [{"parts": [{"text": enhanced_prompt}]}]}
|
| 1484 |
bot_text = ""
|
| 1485 |
http_code = 0
|
| 1486 |
if use_gemini and GEMINI_API_KEY:
|
|
@@ -1577,6 +1533,7 @@ Return ONLY the rewritten guidance."""
|
|
| 1577 |
except Exception as e:
|
| 1578 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 1579 |
|
|
|
|
| 1580 |
# ------------------------------ Ticket description generation ------------------------------
|
| 1581 |
@app.post("/generate_ticket_desc")
|
| 1582 |
async def generate_ticket_desc_ep(input_data: TicketDescInput):
|
|
@@ -1591,7 +1548,7 @@ async def generate_ticket_desc_ep(input_data: TicketDescInput):
|
|
| 1591 |
"Do not include any extra text, comments, or explanations outside the JSON."
|
| 1592 |
)
|
| 1593 |
headers = {"Content-Type": "application/json"}
|
| 1594 |
-
payload = {"contents": [{"parts": [{"text": prompt}]}]}
|
| 1595 |
resp = requests.post(GEMINI_URL, headers=headers, json=payload, timeout=25, verify=GEMINI_SSL_VERIFY)
|
| 1596 |
try:
|
| 1597 |
data = resp.json()
|
|
|
|
| 12 |
from dotenv import load_dotenv
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
+
# ------------------------------ Imports ------------------------------
|
| 16 |
+
# Import shared vocab from KB services
|
| 17 |
+
from services.kb_creation import ACTION_SYNONYMS, MODULE_VOCAB
|
| 18 |
+
from services.kb_creation import (
|
| 19 |
+
collection,
|
| 20 |
+
ingest_documents,
|
| 21 |
+
hybrid_search_knowledge_base,
|
| 22 |
+
get_section_text,
|
| 23 |
+
get_best_steps_section_text,
|
| 24 |
+
get_best_errors_section_text,
|
| 25 |
+
get_escalation_text, # for escalation heading
|
| 26 |
+
bm25_docs,
|
| 27 |
+
)
|
| 28 |
+
from services.login import router as login_router
|
| 29 |
+
from services.generate_ticket import get_valid_token, create_incident
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# ------------------------------ Config ------------------------------
|
| 32 |
VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
app = FastAPI(lifespan=lifespan)
|
| 62 |
+
app.include_router(login_router)
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
origins = ["https://chatbotnova-chatbot-frontend.hf.space"]
|
| 65 |
app.add_middleware(
|
|
|
|
| 174 |
return fams
|
| 175 |
|
| 176 |
|
| 177 |
+
# --- Title keywords global selector (robust when action_tag is missing) ---
|
| 178 |
def _get_steps_by_title_keywords_global(keywords: List[str], prefer_doc: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 179 |
"""
|
| 180 |
Search ALL SOP docs for a 'steps' section whose *section title* contains any of `keywords`.
|
|
|
|
| 215 |
_, best_doc_g, best_sec_g, best_text_g = candidates[0]
|
| 216 |
return best_doc_g, best_sec_g, best_text_g
|
| 217 |
|
| 218 |
+
|
| 219 |
# --- Action-targeted steps selector (uses existing KB metadata) ---
|
| 220 |
def _get_steps_for_action(best_doc: str, actions: list) -> Optional[str]:
|
| 221 |
"""
|
|
|
|
| 279 |
_, best_doc_global, best_text = candidates[0]
|
| 280 |
return best_doc_global, best_text
|
| 281 |
|
| 282 |
+
|
| 283 |
# --- Default section picker when query doesn't reveal action ---
|
| 284 |
def _pick_default_action_section(best_doc: str) -> Optional[str]:
|
| 285 |
"""
|
|
|
|
| 1290 |
next_step_applied = False
|
| 1291 |
next_step_info: Dict[str, Any] = {}
|
| 1292 |
|
| 1293 |
+
# ---------------- Steps branch ----------------
|
| 1294 |
if best_doc and detected_intent == "steps":
|
| 1295 |
context_preformatted = False
|
| 1296 |
full_steps = None
|
|
|
|
| 1436 |
### Output
|
| 1437 |
Return ONLY the rewritten guidance."""
|
| 1438 |
headers = {"Content-Type": "application/json"}
|
| 1439 |
+
payload = {"contents": [{"parts": [{"text": enhanced_prompt}]}]]}
|
| 1440 |
bot_text = ""
|
| 1441 |
http_code = 0
|
| 1442 |
if use_gemini and GEMINI_API_KEY:
|
|
|
|
| 1533 |
except Exception as e:
|
| 1534 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 1535 |
|
| 1536 |
+
|
| 1537 |
# ------------------------------ Ticket description generation ------------------------------
|
| 1538 |
@app.post("/generate_ticket_desc")
|
| 1539 |
async def generate_ticket_desc_ep(input_data: TicketDescInput):
|
|
|
|
| 1548 |
"Do not include any extra text, comments, or explanations outside the JSON."
|
| 1549 |
)
|
| 1550 |
headers = {"Content-Type": "application/json"}
|
| 1551 |
+
payload = {"contents": [{"parts": [{"text": prompt}]}]]}
|
| 1552 |
resp = requests.post(GEMINI_URL, headers=headers, json=payload, timeout=25, verify=GEMINI_SSL_VERIFY)
|
| 1553 |
try:
|
| 1554 |
data = resp.json()
|