Spaces:
Sleeping
Sleeping
| from dotenv import load_dotenv | |
| import requests | |
| import time | |
| import os | |
| from typing import Optional, Dict, Any | |
| load_dotenv() | |
| # --- ServiceNow OAuth / instance config --- | |
| INSTANCE_URL = os.getenv("SERVICENOW_INSTANCE_URL") # e.g., https://your-instance.service-now.com | |
| CLIENT_ID = os.getenv("SERVICENOW_CLIENT_ID") | |
| CLIENT_SECRET = os.getenv("SERVICENOW_CLIENT_SECRET") | |
| # Optional but recommended: provide defaults for create payload (helps BR/ACL allow quick resolution) | |
| DEFAULT_ASSIGNMENT_GROUP_SYSID = os.getenv("SERVICENOW_ASSIGNMENT_GROUP_SYSID") # e.g., Service Desk group sys_id | |
| DEFAULT_CATEGORY = os.getenv("SERVICENOW_DEFAULT_CATEGORY", "inquiry") | |
| DEFAULT_SUBCATEGORY = os.getenv("SERVICENOW_DEFAULT_SUBCATEGORY", "general") | |
| # OAuth token storage (simple in-memory cache) | |
| _token_info: Dict[str, Any] = {"access_token": None, "expires_at": 0} | |
| # Read SSL verify flag from env (default True) | |
| VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes") | |
| def get_oauth_token() -> str: | |
| """ | |
| Fetch a new OAuth token using client credentials flow. | |
| """ | |
| if not INSTANCE_URL or not CLIENT_ID or not CLIENT_SECRET: | |
| raise RuntimeError( | |
| "ServiceNow OAuth env vars are missing (SERVICENOW_INSTANCE_URL / CLIENT_ID / CLIENT_SECRET)." | |
| ) | |
| url = f"{INSTANCE_URL}/oauth_token.do" | |
| data = { | |
| "grant_type": "client_credentials", | |
| "client_id": CLIENT_ID, | |
| "client_secret": CLIENT_SECRET, | |
| "scope": "global", | |
| } | |
| # NOTE: verify=False is OK for local dev; set verify=True in production | |
| resp = requests.post(url, data=data, verify=VERIFY_SSL, timeout=25) # <-- use VERIFY_SSL | |
| if resp.status_code == 200: | |
| tok = resp.json() | |
| _token_info["access_token"] = tok.get("access_token") | |
| _token_info["expires_at"] = time.time() + tok.get("expires_in", 0) | |
| return _token_info["access_token"] | |
| raise Exception(f"Failed to get token: {resp.status_code} - {resp.text}") | |
| def get_valid_token() -> str: | |
| """ | |
| Return a valid token, refreshing if expired. | |
| """ | |
| if _token_info["access_token"] and time.time() < _token_info["expires_at"]: | |
| return _token_info["access_token"] | |
| return get_oauth_token() | |
| def create_incident(short_description: str, description: str) -> Dict[str, Any]: | |
| """ | |
| Create an incident in ServiceNow using OAuth token. | |
| Returns: | |
| - {"number": "<INC...>", "sys_id": "<sys_id>"} on success | |
| - {"error": "<message>", "status_code": <int>} on failure | |
| NOTE: | |
| - Includes optional defaults (category/subcategory/assignment_group) to satisfy common BR/ACL requirements | |
| so your subsequent PATCH to Resolved succeeds. | |
| """ | |
| token = get_valid_token() | |
| url = f"{INSTANCE_URL}/api/now/table/incident" | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Content-Type": "application/json", | |
| "Accept": "application/json", | |
| } | |
| payload: Dict[str, Any] = { | |
| "short_description": short_description, | |
| "description": description, | |
| "urgency": "3", # Medium | |
| "priority": "3", | |
| # Optional defaults—adjust for your instance if needed | |
| "category": DEFAULT_CATEGORY, | |
| "subcategory": DEFAULT_SUBCATEGORY, | |
| "caller_id": os.getenv("SERVICENOW_CALLER_SYSID"), | |
| } | |
| # Only include assignment_group if the env var is set | |
| if DEFAULT_ASSIGNMENT_GROUP_SYSID: | |
| payload["assignment_group"] = DEFAULT_ASSIGNMENT_GROUP_SYSID | |
| # NOTE: verify=False is OK for local dev; set verify=True in production | |
| resp = requests.post(url, headers=headers, json=payload, verify=VERIFY_SSL, timeout=30) # <-- use VERIFY_SSL | |
| if resp.status_code in (200, 201): | |
| try: | |
| result = resp.json().get("result", {}) | |
| except Exception: | |
| # Some instances respond with bare object | |
| result = resp.json() | |
| number = result.get("number") | |
| sys_id = result.get("sys_id") | |
| if number and sys_id: | |
| return {"number": number, "sys_id": sys_id} | |
| # If SN responded but fields missing, surface an error | |
| return {"error": f"Missing Incident fields in response: {resp.text}", "status_code": resp.status_code} | |
| # Failure: return structured error for caller (main.py /incident & /chat will handle this gracefully) | |