From 4e4f10565ce88be696beff04219cbc65791cbff3 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 2 Jan 2026 10:29:22 -0500 Subject: [PATCH 01/35] Add custom subdomain support for OpenAI and Speech Service in Terraform - Added custom_subdomain_name to OpenAI resource for managed identity authentication - Created Speech Service resource with custom subdomain configuration - Added RBAC role assignments for Speech Service (Managed Identity and App Service MI) - Includes Cognitive Services Speech User and Speech Contributor roles - Documentation: Azure Speech managed identity setup guide --- deployers/terraform/main.tf | 71 ++++- ...ure_speech_managed_identity_manul_setup.md | 261 ++++++++++++++++++ 2 files changed, 317 insertions(+), 15 deletions(-) create mode 100644 docs/how-to/azure_speech_managed_identity_manul_setup.md diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index 77b486df..12029506 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -172,6 +172,7 @@ locals { cosmos_db_name = "${var.param_base_name}-${var.param_environment}-cosmos" open_ai_name = "${var.param_base_name}-${var.param_environment}-oai" doc_intel_name = "${var.param_base_name}-${var.param_environment}-docintel" + speech_service_name = "${var.param_base_name}-${var.param_environment}-speech" key_vault_name = "${var.param_base_name}-${var.param_environment}-kv" log_analytics_name = "${var.param_base_name}-${var.param_environment}-la" managed_identity_name = "${var.param_base_name}-${var.param_environment}-id" @@ -625,13 +626,14 @@ resource "azurerm_cosmosdb_account" "cosmos" { # --- Azure OpenAI Service (Cognitive Services) --- resource "azurerm_cognitive_account" "openai" { - count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing - name = local.open_ai_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "OpenAI" - sku_name = "S0" # Standard tier - tags = local.common_tags + count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing + name = local.open_ai_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "OpenAI" + sku_name = "S0" # Standard tier + custom_subdomain_name = local.open_ai_name # Required for managed identity authentication + tags = local.common_tags } # Data source for existing OpenAI instance @@ -643,13 +645,24 @@ data "azurerm_cognitive_account" "existing_openai" { # --- Document Intelligence Service (Cognitive Services) --- resource "azurerm_cognitive_account" "docintel" { - name = local.doc_intel_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "FormRecognizer" - sku_name = "S0" # Standard tier - custom_subdomain_name = local.doc_intel_name # Maps to --custom-domain - tags = local.common_tags + name = local.doc_intel_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "FormRecognizer" + sku_name = "S0" # Standard tier + custom_subdomain_name = local.doc_intel_name # Required for managed identity authentication + tags = local.common_tags +} + +# --- Speech Service (Cognitive Services) --- +resource "azurerm_cognitive_account" "speech" { + name = local.speech_service_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "SpeechServices" + sku_name = "S0" # Standard tier + custom_subdomain_name = local.speech_service_name # Required for managed identity authentication + tags = local.common_tags } # https://medium.com/expert-thinking/mastering-azure-search-with-terraform-a-how-to-guide-7edc3a6b1ee3 @@ -702,6 +715,20 @@ resource "azurerm_role_assignment" "managed_identity_storage_contributor" { principal_id = azurerm_user_assigned_identity.id.principal_id } +# Cognitive Services Speech User on Speech Service +resource "azurerm_role_assignment" "managed_identity_speech_user" { + scope = azurerm_cognitive_account.speech.id + role_definition_name = "Cognitive Services Speech User" + principal_id = azurerm_user_assigned_identity.id.principal_id +} + +# Cognitive Services Speech Contributor on Speech Service +resource "azurerm_role_assignment" "managed_identity_speech_contributor" { + scope = azurerm_cognitive_account.speech.id + role_definition_name = "Cognitive Services Speech Contributor" + principal_id = azurerm_user_assigned_identity.id.principal_id +} + # App Registration Service Principal RBAC # Cognitive Services OpenAI Contributor on OpenAI resource "azurerm_role_assignment" "app_reg_sp_openai_contributor" { @@ -732,13 +759,27 @@ resource "azurerm_role_assignment" "app_service_smi_storage_contributor" { principal_id = azurerm_linux_web_app.app.identity[0].principal_id } -# Storage Blob Data Contributor on Storage Account +# AcrPull on Container Registry resource "azurerm_role_assignment" "acr_pull" { scope = data.azurerm_container_registry.acrregistry.id role_definition_name = "AcrPull" principal_id = azurerm_linux_web_app.app.identity[0].principal_id } +# Cognitive Services Speech User on Speech Service +resource "azurerm_role_assignment" "app_service_smi_speech_user" { + scope = azurerm_cognitive_account.speech.id + role_definition_name = "Cognitive Services Speech User" + principal_id = azurerm_linux_web_app.app.identity[0].principal_id +} + +# Cognitive Services Speech Contributor on Speech Service +resource "azurerm_role_assignment" "app_service_smi_speech_contributor" { + scope = azurerm_cognitive_account.speech.id + role_definition_name = "Cognitive Services Speech Contributor" + principal_id = azurerm_linux_web_app.app.identity[0].principal_id +} + ################################################## # diff --git a/docs/how-to/azure_speech_managed_identity_manul_setup.md b/docs/how-to/azure_speech_managed_identity_manul_setup.md new file mode 100644 index 00000000..7941542d --- /dev/null +++ b/docs/how-to/azure_speech_managed_identity_manul_setup.md @@ -0,0 +1,261 @@ +# Azure Speech Service with Managed Identity Setup + +## Overview + +This guide explains the critical difference between key-based and managed identity authentication when configuring Azure Speech Service, and the required steps to enable managed identity properly. + +## Authentication Methods: Regional vs. Resource-Specific Endpoints + +### Regional Endpoint (Shared Gateway) + +**Endpoint format**: `https://.api.cognitive.microsoft.com` +- Example: `https://eastus2.api.cognitive.microsoft.com` +- This is a **shared endpoint** for all Speech resources in that Azure region +- Acts as a gateway that routes requests to individual Speech resources + +### Resource-Specific Endpoint (Custom Subdomain) + +**Endpoint format**: `https://.cognitiveservices.azure.com` +- Example: `https://simplechat6-dev-speech.cognitiveservices.azure.com` +- This is a **unique endpoint** dedicated to your specific Speech resource +- Requires custom subdomain to be enabled on the resource + +--- + +## Why Regional Endpoint Works with Key but NOT Managed Identity + +### Key-Based Authentication ✅ Works with Regional Endpoint + +When using subscription key authentication: + +```http +POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe +Headers: + Ocp-Apim-Subscription-Key: abc123def456... +``` + +**Why it works:** +1. The subscription key **directly identifies** your specific Speech resource +2. The regional gateway uses the key to look up which resource it belongs to +3. The request is automatically routed to your resource +4. Authorization succeeds because the key proves ownership + +### Managed Identity (AAD Token) ❌ Fails with Regional Endpoint + +When using managed identity authentication: + +```http +POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe +Headers: + Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... +``` + +**Why it fails (returns 400 BadRequest):** +1. The Bearer token proves your App Service identity to Azure AD +2. The token does NOT specify which Speech resource you want to access +3. The regional gateway cannot determine: + - Which specific Speech resource you're authorized for + - Whether your managed identity has RBAC roles on that resource +4. **Result**: The gateway rejects the request with 400 BadRequest + +### Managed Identity ✅ Works with Resource-Specific Endpoint + +When using managed identity with custom subdomain: + +```http +POST https://simplechat6-dev-speech.cognitiveservices.azure.com/speechtotext/transcriptions:transcribe +Headers: + Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... +``` + +**Why it works:** +1. The hostname **itself identifies** your specific Speech resource +2. Azure validates your managed identity Bearer token against that resource's RBAC +3. If your App Service MI has `Cognitive Services Speech User` role → authorized +4. The request proceeds to your dedicated Speech resource instance + +--- + +## Required Setup for Managed Identity + +### Prerequisites + +1. **Azure Speech Service resource** created in your subscription +2. **System-assigned or user-assigned managed identity** on your App Service +3. **RBAC role assignments** on the Speech resource + +### Step 1: Enable Custom Subdomain on Speech Resource + +**Why needed**: By default, Speech resources use the regional endpoint and do NOT have custom subdomains. Managed identity requires the resource-specific endpoint. + +**How to enable**: + +```bash +az cognitiveservices account update \ + --name \ + --resource-group \ + --custom-domain +``` + +**Example**: + +```bash +az cognitiveservices account update \ + --name simplechat6-dev-speech \ + --resource-group sc-simplechat6-dev-rg \ + --custom-domain simplechat6-dev-speech +``` + +**Important notes**: +- Custom subdomain name must be **globally unique** across Azure +- Usually use the same name as your resource: `` +- **One-way operation**: Cannot be disabled once enabled +- After enabling, the resource's endpoint property changes from regional to resource-specific + +**Verify custom subdomain is enabled**: + +```bash +az cognitiveservices account show \ + --name \ + --resource-group \ + --query "{customSubDomainName:properties.customSubDomainName, endpoint:properties.endpoint}" +``` + +Expected output: +```json +{ + "customSubDomainName": "simplechat6-dev-speech", + "endpoint": "https://simplechat6-dev-speech.cognitiveservices.azure.com/" +} +``` + +### Step 2: Assign RBAC Roles to Managed Identity + +Grant your App Service managed identity the necessary roles on the Speech resource: + +```bash +# Get the Speech resource ID +SPEECH_RESOURCE_ID=$(az cognitiveservices account show \ + --name \ + --resource-group \ + --query id -o tsv) + +# Get the App Service managed identity principal ID +MI_PRINCIPAL_ID=$(az webapp identity show \ + --name \ + --resource-group \ + --query principalId -o tsv) + +# Assign Cognitive Services Speech User role (data-plane read access) +az role assignment create \ + --assignee $MI_PRINCIPAL_ID \ + --role "Cognitive Services Speech User" \ + --scope $SPEECH_RESOURCE_ID + +# Assign Cognitive Services Speech Contributor role (if needed for write operations) +az role assignment create \ + --assignee $MI_PRINCIPAL_ID \ + --role "Cognitive Services Speech Contributor" \ + --scope $SPEECH_RESOURCE_ID +``` + +**Verify role assignments**: + +```bash +az role assignment list \ + --assignee $MI_PRINCIPAL_ID \ + --scope $SPEECH_RESOURCE_ID \ + -o table +``` + +### Step 3: Configure Admin Settings + +In the Admin Settings → Search & Extract → Multimedia Support section: + +| Setting | Value | Example | +|---------|-------|---------| +| **Enable Audio File Support** | ✅ Checked | | +| **Speech Service Endpoint** | Resource-specific endpoint (with custom subdomain) | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | +| **Speech Service Location** | Azure region | `eastus2` | +| **Speech Service Locale** | Language locale for transcription | `en-US` | +| **Authentication Type** | Managed Identity | | +| **Speech Service Key** | (Leave empty when using MI) | | + +**Critical**: +- Endpoint must be the resource-specific URL (custom subdomain) +- Do NOT use the regional endpoint for managed identity +- Remove trailing slash from endpoint: ✅ `https://..azure.com` ❌ `https://..azure.com/` + +### Step 4: Test Audio Upload + +1. Upload a short WAV or MP3 file +2. Monitor application logs for transcription progress +3. Expected log output: + ``` + File size: 1677804 bytes + Produced 1 WAV chunks: ['/tmp/tmp_chunk_000.wav'] + [Debug] Transcribing WAV chunk: /tmp/tmp_chunk_000.wav + [Debug] Speech config obtained successfully + [Debug] Received 5 phrases + Creating 3 transcript pages + ``` + +--- + +## Troubleshooting + +### Error: NameResolutionError - Failed to resolve hostname + +**Symptom**: `Failed to resolve 'simplechat6-dev-speech.cognitiveservices.azure.com'` + +**Cause**: Custom subdomain not enabled on Speech resource + +**Solution**: Enable custom subdomain using Step 1 above + +### Error: 400 BadRequest when using MI with regional endpoint + +**Symptom**: `400 Client Error: BadRequest for url: https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe` + +**Cause**: Managed identity requires resource-specific endpoint, not regional + +**Solution**: Update Admin Settings endpoint to use `https://.cognitiveservices.azure.com` + +### Error: 401 Authentication error with MI + +**Symptom**: `WebSocket upgrade failed: Authentication error (401)` + +**Cause**: Missing RBAC role assignments + +**Solution**: Assign required roles using Step 2 above + +### Key auth works but MI fails + +**Diagnosis checklist**: +- [ ] Custom subdomain enabled on Speech resource? +- [ ] Admin Settings endpoint is resource-specific (not regional)? +- [ ] Managed identity has RBAC roles on Speech resource? +- [ ] Authentication Type set to "Managed Identity" in Admin Settings? + +--- + +## Summary + +| Authentication Method | Endpoint Type | Example | Works? | +|----------------------|---------------|---------|--------| +| **Key** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ✅ Yes | +| **Key** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes | +| **Managed Identity** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ❌ No (400 BadRequest) | +| **Managed Identity** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes (with custom subdomain) | + +**Key takeaway**: Managed identity for Azure Cognitive Services data-plane operations requires: +1. Custom subdomain enabled on the resource +2. Resource-specific endpoint configured in your application +3. RBAC roles assigned to the managed identity at the resource scope + +--- + +## References + +- [Azure Cognitive Services custom subdomain documentation](https://learn.microsoft.com/azure/cognitive-services/cognitive-services-custom-subdomains) +- [Authenticate with Azure AD using managed identity](https://learn.microsoft.com/azure/cognitive-services/authentication?tabs=powershell#authenticate-with-azure-active-directory) +- [Azure Speech Service authentication](https://learn.microsoft.com/azure/ai-services/speech-service/rest-speech-to-text-short) From 087fb3dfc2fbaa66007e993df40ee776dcdbe5e0 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 18:06:30 -0500 Subject: [PATCH 02/35] feat: Add ServiceNow integration documentation and bug fixes - Add comprehensive ServiceNow integration guide with OAuth 2.0 setup - Include OpenAPI specifications for Incident Management and Knowledge Base APIs - Add agent instructions for ServiceNow support agent - Fix GROUP_ACTION_OAUTH_SCHEMA_MERGING: Ensure additionalFields preserved during schema merge - Fix GROUP_AGENT_LOADING: Improve group agent loading reliability - Fix OPENAPI_BASIC_AUTH: Support basic authentication in OpenAPI actions - Fix AZURE_AI_SEARCH_TEST_CONNECTION: Improve AI Search connection testing - Update version to 0.236.012 --- application/single_app/config.py | 2 +- .../single_app/route_backend_plugins.py | 12 + .../single_app/route_backend_settings.py | 71 +- .../single_app/semantic_kernel_loader.py | 109 ++- .../openapi_plugin_factory.py | 46 +- .../single_app/static/images/custom_logo.png | Bin 11705 -> 7586 bytes .../static/images/custom_logo_dark.png | Bin 13770 -> 0 bytes .../AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 226 ++++++ .../GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 403 +++++++++ docs/fixes/GROUP_AGENT_LOADING_FIX.md | 241 ++++++ docs/fixes/OPENAPI_BASIC_AUTH_FIX.md | 205 +++++ .../ServiceNow/SERVICENOW_INTEGRATION.md | 762 ++++++++++++++++++ .../ServiceNow/SERVICENOW_OAUTH_SETUP.md | 503 ++++++++++++ .../now_knowledge_latest_spec_sample.yaml | 33 + .../now_table_api_latest_spec_sample.yaml | 331 ++++++++ .../sample_now_knowledge_latest_spec.yaml | 267 ++++++ ...e_now_knowledge_latest_spec_basicauth.yaml | 320 ++++++++ .../sample_servicenow_incident_api.yaml | 565 +++++++++++++ ...ple_servicenow_incident_api_basicauth.yaml | 570 +++++++++++++ ...enow_incident_api - basic auth sample.yaml | 498 ++++++++++++ .../servicenow_agent_instructions.txt | 262 ++++++ 21 files changed, 5333 insertions(+), 93 deletions(-) delete mode 100644 application/single_app/static/images/custom_logo_dark.png create mode 100644 docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md create mode 100644 docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md create mode 100644 docs/fixes/GROUP_AGENT_LOADING_FIX.md create mode 100644 docs/fixes/OPENAPI_BASIC_AUTH_FIX.md create mode 100644 docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md create mode 100644 docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml create mode 100644 docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt diff --git a/application/single_app/config.py b/application/single_app/config.py index 0596e3ca..caf09fc8 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -88,7 +88,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.236.011" +VERSION = "0.236.012" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/route_backend_plugins.py b/application/single_app/route_backend_plugins.py index 01d448b5..6f24c932 100644 --- a/application/single_app/route_backend_plugins.py +++ b/application/single_app/route_backend_plugins.py @@ -458,6 +458,12 @@ def create_group_action_route(): for key in ('group_id', 'last_updated', 'user_id', 'is_global', 'is_group', 'scope'): payload.pop(key, None) + # Merge with schema to ensure all required fields are present (same as global actions) + schema_dir = os.path.join(current_app.root_path, 'static', 'json', 'schemas') + merged = get_merged_plugin_settings(payload.get('type'), payload, schema_dir) + payload['metadata'] = merged.get('metadata', payload.get('metadata', {})) + payload['additionalFields'] = merged.get('additionalFields', payload.get('additionalFields', {})) + try: saved = save_group_action(active_group, payload) except Exception as exc: @@ -511,6 +517,12 @@ def update_group_action_route(action_id): except ValueError as exc: return jsonify({'error': str(exc)}), 400 + # Merge with schema to ensure all required fields are present (same as global actions) + schema_dir = os.path.join(current_app.root_path, 'static', 'json', 'schemas') + schema_merged = get_merged_plugin_settings(merged.get('type'), merged, schema_dir) + merged['metadata'] = schema_merged.get('metadata', merged.get('metadata', {})) + merged['additionalFields'] = schema_merged.get('additionalFields', merged.get('additionalFields', {})) + try: saved = save_group_action(active_group, merged) except Exception as exc: diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index be182e93..9df4b3ee 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -761,42 +761,45 @@ def _test_azure_ai_search_connection(payload): """Attempt to connect to Azure Cognitive Search (or APIM-wrapped).""" enable_apim = payload.get('enable_apim', False) - if enable_apim: - apim_data = payload.get('apim', {}) - endpoint = apim_data.get('endpoint') # e.g. https://my-apim.azure-api.net/search - subscription_key = apim_data.get('subscription_key') - url = f"{endpoint.rstrip('/')}/indexes?api-version=2023-11-01" - headers = { - 'api-key': subscription_key, - 'Content-Type': 'application/json' - } - else: - direct_data = payload.get('direct', {}) - endpoint = direct_data.get('endpoint') # e.g. https://.search.windows.net - key = direct_data.get('key') - url = f"{endpoint.rstrip('/')}/indexes?api-version=2023-11-01" - - if direct_data.get('auth_type') == 'managed_identity': - credential_scopes=search_resource_manager + "/.default" - arm_scope = credential_scopes - credential = DefaultAzureCredential() - arm_token = credential.get_token(arm_scope).token - headers = { - 'Authorization': f'Bearer {arm_token}', - 'Content-Type': 'application/json' - } + try: + if enable_apim: + apim_data = payload.get('apim', {}) + endpoint = apim_data.get('endpoint') + subscription_key = apim_data.get('subscription_key') + + # Use SearchIndexClient for APIM + credential = AzureKeyCredential(subscription_key) + client = SearchIndexClient(endpoint=endpoint, credential=credential) else: - headers = { - 'api-key': key, - 'Content-Type': 'application/json' - } - - # A small GET to /indexes to verify we have connectivity - resp = requests.get(url, headers=headers, timeout=10) - if resp.status_code == 200: + direct_data = payload.get('direct', {}) + endpoint = direct_data.get('endpoint') + key = direct_data.get('key') + + if direct_data.get('auth_type') == 'managed_identity': + credential = DefaultAzureCredential() + # For managed identity, use the SDK which handles authentication properly + if AZURE_ENVIRONMENT in ("usgovernment", "custom"): + client = SearchIndexClient( + endpoint=endpoint, + credential=credential, + audience=search_resource_manager + ) + else: + # For public cloud, don't use audience parameter + client = SearchIndexClient( + endpoint=endpoint, + credential=credential + ) + else: + credential = AzureKeyCredential(key) + client = SearchIndexClient(endpoint=endpoint, credential=credential) + + # Test by listing indexes (simple operation to verify connectivity) + indexes = list(client.list_indexes()) return jsonify({'message': 'Azure AI search connection successful'}), 200 - else: - raise Exception(f"Azure AI search connection error: {resp.status_code} - {resp.text}") + + except Exception as e: + return jsonify({'error': f'Azure AI search connection error: {str(e)}'}), 500 def _test_azure_doc_intelligence_connection(payload): diff --git a/application/single_app/semantic_kernel_loader.py b/application/single_app/semantic_kernel_loader.py index 35d35965..9248ad6b 100644 --- a/application/single_app/semantic_kernel_loader.py +++ b/application/single_app/semantic_kernel_loader.py @@ -1180,65 +1180,44 @@ def load_user_semantic_kernel(kernel: Kernel, settings, user_id: str, redis_clie ensure_agents_migration_complete(user_id) agents_cfg = get_personal_agents(user_id) - print(f"[SK Loader] User settings found {len(agents_cfg)} agents for user '{user_id}'") + print(f"[SK Loader] User settings found {len(agents_cfg)} personal agents for user '{user_id}'") - # Always mark user agents as is_global: False + # Always mark personal agents as is_global: False, is_group: False for agent in agents_cfg: agent['is_global'] = False - - # Append selected group agent (if any) to the candidate list so downstream selection logic can resolve it - selected_agent_data = selected_agent if isinstance(selected_agent, dict) else {} - selected_agent_is_group = selected_agent_data.get('is_group', False) - if selected_agent_is_group: - resolved_group_id = selected_agent_data.get('group_id') - try: - active_group_id = require_active_group(user_id) - if not resolved_group_id: - resolved_group_id = active_group_id - elif resolved_group_id != active_group_id: - debug_print( - f"[SK Loader] Selected group agent references group {resolved_group_id}, active group is {active_group_id}." - ) - except ValueError as err: - debug_print(f"[SK Loader] No active group available while loading group agent: {err}") - if not resolved_group_id: - log_event( - "[SK Loader] Group agent selected but no active group in settings.", - level=logging.WARNING - ) - - if resolved_group_id: - agent_identifier = selected_agent_data.get('id') or selected_agent_data.get('name') - group_agent_cfg = None - if agent_identifier: - group_agent_cfg = get_group_agent(resolved_group_id, agent_identifier) - if not group_agent_cfg: - # Fallback: search by name across group agents if ID lookup failed - for candidate in get_group_agents(resolved_group_id): - if candidate.get('name') == selected_agent_data.get('name'): - group_agent_cfg = candidate - break - - if group_agent_cfg: - group_agent_cfg['is_global'] = False - group_agent_cfg['is_group'] = True - group_agent_cfg.setdefault('group_id', resolved_group_id) - group_agent_cfg['group_name'] = selected_agent_data.get('group_name') - agents_cfg.append(group_agent_cfg) - log_event( - f"[SK Loader] Added group agent '{group_agent_cfg.get('name')}' from group {resolved_group_id} to candidate list.", - level=logging.INFO - ) - else: - log_event( - f"[SK Loader] Selected group agent '{selected_agent_data.get('name')}' not found for group {resolved_group_id}.", - level=logging.WARNING - ) - else: - log_event( - "[SK Loader] Unable to resolve group ID for selected group agent; skipping group agent load.", - level=logging.WARNING - ) + agent['is_group'] = False + + # Load group agents from all groups the user is a member of + from functions_group import get_user_groups + from functions_group_agents import get_group_agents + + user_groups = [] # Initialize to empty list + try: + user_groups = get_user_groups(user_id) + print(f"[SK Loader] User '{user_id}' is a member of {len(user_groups)} groups") + + group_agent_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_agents = get_group_agents(group_id) + for group_agent in group_agents: + group_agent['is_global'] = False + group_agent['is_group'] = True + group_agent['group_id'] = group_id + group_agent['group_name'] = group_name + agents_cfg.append(group_agent) + group_agent_count += 1 + print(f"[SK Loader] Loaded {len(group_agents)} agents from group '{group_name}' (id: {group_id})") + + if group_agent_count > 0: + log_event(f"[SK Loader] Loaded {group_agent_count} group agents from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) + except Exception as e: + log_event(f"[SK Loader] Error loading group agents for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) + user_groups = [] # Reset to empty on error + + print(f"[SK Loader] Total agents loaded: {len(agents_cfg)} (personal + group) for user '{user_id}'") # PATCH: Merge global agents if enabled merge_global = settings.get('merge_global_semantic_kernel_with_workspace', False) @@ -1278,9 +1257,27 @@ def load_user_semantic_kernel(kernel: Kernel, settings, user_id: str, redis_clie "agents": agents_cfg }, level=logging.INFO) + # Ensure migration is complete (will migrate any remaining legacy data) ensure_actions_migration_complete(user_id) plugin_manifests = get_personal_actions(user_id, return_type=SecretReturnType.NAME) + + # Load group actions from all groups the user is a member of + try: + group_action_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_actions = get_group_actions(group_id, return_type=SecretReturnType.NAME) + plugin_manifests.extend(group_actions) + group_action_count += len(group_actions) + print(f"[SK Loader] Loaded {len(group_actions)} actions from group '{group_name}' (id: {group_id})") + + if group_action_count > 0: + log_event(f"[SK Loader] Loaded {group_action_count} group actions from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) + except Exception as e: + log_event(f"[SK Loader] Error loading group actions for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) # PATCH: Merge global plugins if enabled if merge_global: diff --git a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py index 3380c208..8f8a4d84 100644 --- a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py +++ b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py @@ -125,15 +125,57 @@ def _get_local_file_path(cls, config: Dict[str, Any]) -> str: @classmethod def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: """Extract authentication configuration from plugin config.""" + from functions_debug import debug_print + auth_config = config.get('auth', {}) + debug_print(f"[Factory] Initial auth_config: {auth_config}") if not auth_config: return {} auth_type = auth_config.get('type', 'none') + debug_print(f"[Factory] auth_type: {auth_type}") if auth_type == 'none': return {} - # Return the auth config as-is since the OpenApiPlugin already handles - # the different auth types + # Check if this is basic auth stored in the 'key' field format + # Simple Chat stores basic auth as: auth.type='key', auth.key='username:password', additionalFields.auth_method='basic' + additional_fields = config.get('additionalFields', {}) + auth_method = additional_fields.get('auth_method', '') + debug_print(f"[Factory] additionalFields.auth_method: {auth_method}") + + if auth_type == 'key' and auth_method == 'basic': + # Extract username and password from the combined key + key = auth_config.get('key', '') + debug_print(f"[Factory] Applying basic auth transformation") + if ':' in key: + username, password = key.split(':', 1) + return { + 'type': 'basic', + 'username': username, + 'password': password + } + else: + # Malformed basic auth key + return {} + + # For bearer tokens stored as 'key' type + if auth_type == 'key' and auth_method == 'bearer': + token = auth_config.get('key', '') + debug_print(f"[Factory] *** APPLYING BEARER AUTH TRANSFORMATION - token: {token[:20]}...") + return { + 'type': 'bearer', + 'token': token + } + + # For OAuth2 stored as 'key' type + if auth_type == 'key' and auth_method == 'oauth2': + debug_print(f"[Factory] Applying OAuth2 auth transformation") + return { + 'type': 'bearer', # OAuth2 tokens are typically bearer tokens + 'token': auth_config.get('key', '') + } + + debug_print(f"[Factory] Returning auth as-is: {auth_config}") + # Return the auth config as-is for other auth types return auth_config diff --git a/application/single_app/static/images/custom_logo.png b/application/single_app/static/images/custom_logo.png index 45a99fd35f8834db8920ea29bd2bfee10fe754d2..ab32f65fe249cf825c6b6e2f464bb3da11a73eba 100644 GIT binary patch literal 7586 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
txT=*o8&jKB+g$Gzp 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 Kc$KK}xBmg`z(eK$ diff --git a/application/single_app/static/images/custom_logo_dark.png b/application/single_app/static/images/custom_logo_dark.png deleted file mode 100644 index b3beb694201dc8b371e45c973895a95e211eab8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13770 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 diff --git a/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md new file mode 100644 index 00000000..0d507ecc --- /dev/null +++ b/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md @@ -0,0 +1,226 @@ +# Azure AI Search Test Connection Fix + +## Issue Description + +When clicking the "Test Azure AI Search Connection" button on the App Settings "Search & Extract" page with **managed identity authentication** enabled, the test connection failed with the following error: + +**Original Error Message:** +``` +NameError: name 'search_resource_manager' is not defined +``` + +**Environment Configuration:** +- Authentication Type: Managed Identity +- Azure Environment: `public` (set in .env file) +- Error occurred because the code tried to reference `search_resource_manager` which wasn't defined for public cloud + +**Root Cause:** The old implementation used a REST API approach that required the `search_resource_manager` variable to construct authentication tokens. This variable wasn't defined for the public cloud environment, causing the initial error. Even if defined, the REST API approach with bearer tokens doesn't work properly with Azure AI Search's managed identity authentication. + +## Root Cause Analysis + +The old implementation used a **REST API approach with manually acquired bearer tokens**, which is fundamentally incompatible with how Azure AI Search handles managed identity authentication on the data plane. + +### Why the Old Approach Failed + +Azure AI Search's data plane operations don't properly accept bearer tokens acquired through standard `DefaultAzureCredential.get_token()` flows and passed as HTTP Authorization headers. The authentication mechanism works differently: + +```python +# OLD IMPLEMENTATION - FAILED ❌ +credential = DefaultAzureCredential() +arm_scope = f"{search_resource_manager}/.default" +token = credential.get_token(arm_scope).token + +headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" +} +response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) +# Returns: 403 Forbidden +``` + +**Problems with this approach:** +1. Azure AI Search requires SDK-specific authentication handling +2. Bearer tokens from `get_token()` are rejected by the Search service +3. Token scope and refresh logic need specialized handling +4. This issue occurs in **all Azure environments** (public, government, custom) + +### Why Other Services Work with REST API + Bearer Tokens + +Some Azure services accept bearer tokens in REST API calls, but Azure AI Search requires the SDK to: +1. Acquire tokens using the correct scope and flow +2. Handle token refresh automatically +3. Use Search-specific authentication headers +4. Properly negotiate with the Search service's auth layer + +## Technical Details + +### Files Modified + +**File:** `route_backend_settings.py` +**Function:** `_test_azure_ai_search_connection(payload)` +**Lines:** 760-796 + +### The Solution + +Instead of trying to define `search_resource_manager` for public cloud, the fix was to **replace the REST API approach entirely with the SearchIndexClient SDK**, which handles authentication correctly without needing the `search_resource_manager` variable. + +### Code Changes Summary + +**Before (REST API approach):** +```python +def _test_azure_ai_search_connection(payload): + # ... setup code ... + + if direct_data.get('auth_type') == 'managed_identity': + credential = DefaultAzureCredential() + arm_scope = f"{search_resource_manager}/.default" + token = credential.get_token(arm_scope).token + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) + # ❌ Returns 403 Forbidden +``` + +**After (SDK approach):** +```python +def _test_azure_ai_search_connection(payload): + # ... setup code ... + + if direct_data.get('auth_type') == 'managed_identity': + credential = DefaultAzureCredential() + + # Use SDK which handles authentication properly + if AZURE_ENVIRONMENT in ("usgovernment", "custom"): + client = SearchIndexClient( + endpoint=endpoint, + credential=credential, + audience=search_resource_manager + ) + else: + # For public cloud, don't use audience parameter + client = SearchIndexClient( + endpoint=endpoint, + credential=credential + ) + + # Test by listing indexes (simple operation to verify connectivity) + indexes = list(client.list_indexes()) + # ✅ Works correctly +``` + +### Key Implementation Details + +1. **Replaced REST API with SearchIndexClient SDK** + - Uses `SearchIndexClient` from `azure.search.documents` + - SDK handles authentication internally + - Properly manages token acquisition and refresh + +2. **Environment-Specific Configuration** + - **Azure Government/Custom:** Requires `audience` parameter + - **Azure Public Cloud:** Omits `audience` parameter + - Matches pattern used throughout codebase + +3. **Consistent with Other Functions** + - Aligns with `get_index_client()` implementation (line 484) + - Matches SearchClient initialization in `config.py` (lines 584-619) + - All other search operations already use SDK approach + +## Testing Approach + +### Prerequisites +- Service principal must have **"Search Index Data Contributor"** RBAC role +- Permissions must propagate (5-10 minutes after assignment) + +### RBAC Role Assignment Command +```bash +az role assignment create \ + --assignee \ + --role "Search Index Data Contributor" \ + --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ +``` + +### Verification +```bash +az role assignment list \ + --assignee \ + --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ \ + --output table +``` + +## Impact Analysis + +### What Changed +- **Only the test connection function** was affected +- No changes needed to actual search operations (indexing, querying, etc.) +- All other search functionality already used correct SDK approach + +### Why Other Search Operations Weren't Affected +All production search operations throughout the codebase already use the SDK: +- `SearchClient` for querying indexes +- `SearchIndexClient` for managing indexes +- `get_index_client()` helper function +- Index initialization in `config.py` + +**Only the test connection function used the failed REST API approach.** + +## Validation + +### Before Fix +- ✅ Authentication succeeded (no credential errors) +- ✅ Token acquisition worked +- ❌ Azure AI Search rejected bearer token (403 Forbidden) +- ❌ Test connection failed + +### After Fix +- ✅ Authentication succeeds +- ✅ SDK handles token acquisition properly +- ✅ Azure AI Search accepts SDK authentication +- ✅ Test connection succeeds (with proper RBAC permissions) + +## Configuration Requirements + +### Public Cloud (.env) +```ini +AZURE_ENVIRONMENT=public +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_TENANT_ID= +AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity +AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.net +``` + +### Azure Government (.env) +```ini +AZURE_ENVIRONMENT=usgovernment +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_TENANT_ID= +AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity +AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.us +``` + +## Related Changes + +**No config.py changes were made.** The fix was entirely in the route_backend_settings.py file by switching from REST API to SDK approach. + +The SDK approach eliminates the need for the `search_resource_manager` variable in public cloud because: +- The SearchIndexClient handles authentication internally +- No manual token acquisition is needed +- The SDK knows the correct endpoints and scopes automatically + +## Version Information + +**Version Implemented:** 0.235.004 + +## References + +- Azure AI Search SDK Documentation: https://learn.microsoft.com/python/api/azure-search-documents +- Azure RBAC for Search: https://learn.microsoft.com/azure/search/search-security-rbac +- DefaultAzureCredential: https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential + +## Summary + +The fix replaces manual REST API calls with the proper Azure Search SDK (`SearchIndexClient`), which correctly handles managed identity authentication for Azure AI Search. This aligns the test function with all other search operations in the codebase that already use the SDK approach successfully. diff --git a/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md new file mode 100644 index 00000000..196bc132 --- /dev/null +++ b/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -0,0 +1,403 @@ +# Group Action OAuth Authentication and Schema Merging Fix + +## Header Information + +**Fix Title:** Group Actions Missing `additionalFields` Causing OAuth Authentication Failures +**Issue Description:** Group actions were missing the `additionalFields` property entirely, preventing OAuth bearer token authentication from working despite having the same configuration as working global actions. +**Root Cause:** Group action backend routes did not call `get_merged_plugin_settings()` to merge UI form data with schema defaults, while global action routes did. This caused group actions to be saved without authentication configuration fields. +**Version Implemented:** 0.235.028 +**Date:** January 22, 2026 + +## Problem Statement + +### Symptoms +When a group action was configured with OAuth bearer token authentication: +- Action execution returned **HTTP 401 Unauthorized** errors +- ServiceNow API responded: `{"error":{"message":"User is not authenticated"}}` +- UI displayed `additionalFields: {}` (empty object) when editing group action +- Global action with identical configuration showed populated `additionalFields` and worked correctly +- Bearer token header was not being sent in API requests + +### Impact +- **Severity:** High - OAuth authentication completely non-functional for group actions +- **Affected Users:** All users attempting to use group actions with OAuth/Bearer token authentication +- **Workaround:** Use global actions instead of group actions (not scalable) + +### Evidence from Logs +``` +[DEBUG] Auth type: bearer +[DEBUG] Token available: True +[DEBUG] Added bearer auth: EfP7otqXmV... +[DEBUG] Making request to https://dev222288.service-now.com/api/now/table/incident +[DEBUG] Request headers: {'Authorization': 'Bearer EfP7otqXmV...', ...} +[DEBUG] Response status: 401 +[DEBUG] Response text: {"error":{"message":"User is not authenticated",...}} +``` + +**Critical Discovery:** When comparing global vs group action data: +- **Global action** (working): `additionalFields: {auth_method: 'bearer', base_url: '...', ...}` +- **Group action** (failing): `additionalFields: {}` ← Empty object! + +## Root Cause Analysis + +### Backend Route Disparity + +#### Global Action Routes (Working) +**File:** `route_backend_plugins.py` - Lines 666-667 (add_plugin route) + +```python +# Global action creation route +merged = get_merged_plugin_settings( + plugin_type, + current_settings=additionalFields, + schema_dir=schema_dir +) +``` + +**Result:** UI form data is merged with schema defaults, preserving authentication configuration sent from JavaScript. + +#### Group Action Routes (Broken - Before Fix) +**File:** `route_backend_plugins.py` - Lines 430-470, 485-530 + +```python +# Group action creation/update routes - BEFORE FIX +# NO CALL to get_merged_plugin_settings() +# additionalFields saved directly from request without merging +``` + +**Result:** `additionalFields` data from UI was not being preserved, resulting in empty objects. + +### Data Flow Architecture + +The fix revealed the actual data flow for authentication configuration: + +1. **UI Layer** (`plugin_modal_stepper.js` line 1537): + ```javascript + additionalFields.auth_method = 'bearer'; // Set by JavaScript based on dropdown + ``` + +2. **HTTP POST** to backend: + ```json + { + "name": "action_name", + "auth": {"type": "key"}, + "additionalFields": { + "auth_method": "bearer", + "base_url": "https://dev222288.service-now.com/api/now" + } + } + ``` + +3. **Backend Processing** - `get_merged_plugin_settings()`: + - **If schema file exists:** Merge UI data with schema defaults + - **If schema file missing:** Return UI data unchanged (graceful fallback) + - **If function not called:** Data lost! + +4. **Storage:** Cosmos DB saves merged data + +### Why Global Actions Worked Without Schema File + +**Key Insight:** The `openapi_plugin.additional_settings.schema.json` file **never existed** for global actions either! + +Global actions worked because: +1. Backend routes **called** `get_merged_plugin_settings()` +2. Function detected missing schema file +3. **Graceful fallback** (lines 110-114 in `functions_plugins.py`): + ```python + else: + result[nested_key] = current_val # Return UI data unchanged + ``` +4. UI data passed through and was saved correctly + +Group actions failed because: +1. Backend routes **did not call** the merge function at all +2. `additionalFields` from UI was discarded +3. Empty object `{}` saved to database +4. OAuth configuration lost + +## Technical Details + +### Files Modified + +1. **`route_backend_plugins.py`** (Lines 430-530) + - **Line 461-463** (create_group_action_route): Added schema merging + - **Line 520-522** (update_group_action_route): Added schema merging + - **Parity achieved:** Both global and group routes now call `get_merged_plugin_settings()` + +2. **`config.py`** + - Updated VERSION from "0.235.027" to "0.235.028" + +### Code Changes + +#### Group Action Creation Route - BEFORE +```python +def create_group_action_route(user_id, group_id): + """Create new group action""" + data = request.get_json() + # ... validation ... + + # Direct save without merging + saved_plugin = save_group_action( + user_id=user_id, + group_id=group_id, + plugin_data=data # additionalFields lost here! + ) +``` + +#### Group Action Creation Route - AFTER (Fixed) +```python +def create_group_action_route(user_id, group_id): + """Create new group action""" + data = request.get_json() + # ... validation ... + + # NEW: Merge additionalFields with schema defaults (lines 461-463) + merged = get_merged_plugin_settings( + plugin_type=data.get('type', 'openapi'), + current_settings=data.get('additionalFields', {}), + schema_dir=schema_dir + ) + data['additionalFields'] = merged + + saved_plugin = save_group_action( + user_id=user_id, + group_id=group_id, + plugin_data=data # Now includes preserved auth config! + ) +``` + +**Same fix applied to:** +- `update_group_action_route()` (lines 520-522) + +### Graceful Fallback Behavior + +**File:** `functions_plugins.py` (Lines 92-115) + +```python +def get_merged_plugin_settings(plugin_type, current_settings, schema_dir): + """ + Merge plugin settings with schema defaults. + + If schema file doesn't exist: returns current_settings unchanged. + This is intentional - allows UI-driven configuration. + """ + schema_path = os.path.join(schema_dir, f"{plugin_type}.additional_settings.schema.json") + + if not os.path.exists(schema_path): + # Graceful fallback - return UI data as-is (lines 110-114) + result = {} + for nested_key in current_settings: + result[nested_key] = current_settings[nested_key] # Preserve UI data + return result + + # If schema exists, merge with defaults + # ... +``` + +**Design Decision:** Schema files are **optional** - the system works perfectly with UI-driven configuration via graceful fallback. + +## Solution Implemented + +### Fix Strategy +1. ✅ Add `get_merged_plugin_settings()` calls to group action routes (parity with global routes) +2. ✅ Rely on UI-driven configuration + backend graceful fallback (proven approach) +3. ✅ Require recreation of existing group actions to populate `additionalFields` + +### Architecture Result + +**Both global and group routes now have identical behavior:** + +1. **UI sends complete `additionalFields`** from form +2. **Backend calls `get_merged_plugin_settings()`** for parity +3. **Function detects no schema file** exists +4. **Graceful fallback returns UI data unchanged** +5. **Complete authentication config saved** to database + +**Benefits:** +- ✅ Simple: UI drives configuration, backend preserves it +- ✅ Proven: Global actions validate this approach +- ✅ Maintainable: No schema files to keep in sync +- ✅ Flexible: Easy to extend authentication types in UI + +## Validation + +### Test Procedure +1. Delete existing group action (has empty `additionalFields`) +2. Create new group action via UI: + - Type: OpenAPI + - Upload ServiceNow spec + - Base URL: `https://dev222288.service-now.com/api/now` + - Authentication: **Bearer Token** (dropdown selection) + - Token: `EfP7otqXmVmg06xfB9igagxL6Pjir7ewv99sZyMqYdzImlerPt9rHM1T1_L8cCEeWZAuWUV0GPDP2eZ56XWoEQ` +3. UI JavaScript sets `additionalFields.auth_method = 'bearer'` (line 1537) +4. Backend merge function preserves UI data via fallback +5. Action saved with complete authentication configuration + +### Expected Results +- ✅ Group action `additionalFields` populated: `{auth_method: 'bearer', base_url: '...', ...}` +- ✅ ServiceNow API calls return **HTTP 200** instead of 401 +- ✅ Authorization header sent: `Bearer EfP7otqXmV...` +- ✅ Group agent successfully queries ServiceNow incidents +- ✅ Edit group action page displays authentication fields correctly + +## Impact Analysis + +### Before Fix +- **Global actions:** ✅ Working - routes call merge function +- **Group actions:** ❌ Broken - routes don't call merge function +- **Result:** OAuth authentication impossible for group actions + +### After Fix +- **Global actions:** ✅ Working - routes call merge function → fallback preserves UI data +- **Group actions:** ✅ Working - routes call merge function → fallback preserves UI data +- **Result:** Complete parity, OAuth authentication works for both + +### Breaking Changes +**None** - This is a pure fix with backward compatibility: +- Existing global actions continue working (unchanged code path) +- **New/recreated** group actions now work correctly +- Existing broken group actions remain broken until recreated (user action required) + +## Lessons Learned + +### Key Insights +1. **UI is source of truth for authentication config** - Backend preserves what UI sends +2. **Graceful fallback is a feature, not a bug** - Enables UI-driven configuration +3. **Code parity prevents subtle bugs** - Global and group routes should be identical +4. **Testing existing functionality reveals architecture** - Global actions proved UI approach works + +### Best Practices Reinforced +- **Investigate working code before making changes** - Global actions showed the pattern +- **Prefer simplicity** - UI-driven configuration simpler than complex schema systems +- **Document data flows** - Understanding UI → Backend → DB flow was crucial +- **Test parity** - If code paths differ, investigate why + +## Related Documentation +- **[Group Agent Loading Fix](./GROUP_AGENT_LOADING_FIX.md)** - Prerequisites for this fix (v0.235.027) +- **ServiceNow OAuth Setup** - Configuration instructions for OAuth 2.0 bearer tokens +- **Plugin Modal Stepper** - UI component responsible for authentication form (`plugin_modal_stepper.js`) + +## Future Considerations + +### ⚠️ CRITICAL: OAuth 2.0 Token Expiration Limitation + +**Current Implementation Status:** +- ✅ **Bearer token authentication works correctly** - tokens are sent properly in HTTP headers +- ❌ **No automatic token refresh** - requires manual regeneration when expired +- ⚠️ **Production limitation** - not suitable for production use without enhancement + +**The Problem:** +ServiceNow OAuth access tokens expire after a configured lifespan (e.g., 3,600 seconds = 1 hour). The current Simple Chat implementation: + +1. **Stores static bearer tokens** - copied from ServiceNow and hardcoded in action configuration +2. **No expiration tracking** - doesn't know when token will expire +3. **No refresh mechanism** - can't automatically request new tokens +4. **Manual workaround required** - users must regenerate and update token every hour + +**Example Failure:** +``` +Request: GET https://dev222288.service-now.com/api/now/table/incident +Headers: Authorization: Bearer EfP7otqXmV... (expired token) +Response: HTTP 401 - {"error":{"message":"User is not authenticated"}} +``` + +**Temporary Testing Workaround:** +- Increase ServiceNow "Access Token Lifespan" to longer duration (e.g., 86,400 seconds = 24 hours) +- Regenerate token before expiration +- **Not suitable for production environments** + +**Proper Solution Required (Future Enhancement):** + +To make OAuth 2.0 authentication production-ready, Simple Chat needs to implement the OAuth 2.0 Client Credentials flow with automatic token refresh: + +#### Required Components: + +1. **Store OAuth Client Credentials** (Not Bearer Token): + ```json + { + "auth_type": "oauth2_client_credentials", + "client_id": "565d53a80dfe4cb89b8869fd1d977308", + "client_secret": "[encrypted_secret]", + "token_endpoint": "https://dev222288.service-now.com/oauth_token.do", + "scope": "useraccount" + } + ``` + +2. **Token Storage with Expiration Tracking**: + ```python + { + "access_token": "EfP7otqXmV...", + "refresh_token": "abc123...", + "expires_at": "2026-01-22T20:17:39Z", # Timestamp + "token_type": "bearer" + } + ``` + +3. **Automatic Token Refresh Logic**: + ```python + def get_valid_token(action_config): + """Get valid token, refreshing if expired""" + if token_expired(action_config): + # Call ServiceNow OAuth token endpoint + response = requests.post( + action_config['token_endpoint'], + data={ + 'grant_type': 'client_credentials', + 'client_id': action_config['client_id'], + 'client_secret': decrypt(action_config['client_secret']) + } + ) + # Update stored token with new access_token and expires_at + update_token_storage(response.json()) + + return get_current_token() + ``` + +4. **Pre-Request Token Validation**: + ```python + # Before each API call in openapi_plugin.py + if auth_config['type'] == 'oauth2_client_credentials': + auth_config['token'] = get_valid_token(auth_config) + headers['Authorization'] = f"Bearer {auth_config['token']}" + ``` + +5. **Secure Secret Storage**: + - Store client secrets in Azure Key Vault (not in Cosmos DB) + - Use Managed Identity for Key Vault access + - Encrypt secrets at rest + +#### Implementation Tasks: + +- [ ] **UI Changes**: Add OAuth 2.0 configuration form (Client ID, Secret, Token Endpoint) +- [ ] **Backend Changes**: + - [ ] Create `oauth2_token_manager.py` module for token lifecycle management + - [ ] Implement token refresh logic with expiration checking + - [ ] Add Key Vault integration for client secret storage + - [ ] Update `openapi_plugin_factory.py` to detect OAuth 2.0 auth type + - [ ] Modify HTTP request preparation to request fresh tokens +- [ ] **Database Schema**: Add token storage fields (access_token, refresh_token, expires_at) +- [ ] **Testing**: End-to-end testing with real OAuth 2.0 endpoints and token expiration scenarios +- [ ] **Documentation**: Update user guide with OAuth 2.0 setup instructions + +#### References: +- [OAuth 2.0 Client Credentials Grant](https://oauth.net/2/grant-types/client-credentials/) +- [ServiceNow OAuth 2.0 Documentation](https://docs.servicenow.com/bundle/washingtondc-platform-security/page/administer/security/concept/c_OAuthApplications.html) +- [Azure Key Vault for Secret Management](https://learn.microsoft.com/azure/key-vault/general/overview) + +**Estimated Effort:** 2-3 weeks for complete implementation and testing + +**Priority:** Medium - Current manual workaround functional for testing/development, critical for production deployment + +--- + +### Monitoring +Track authentication failures by action type to detect similar issues: +```python +# Example monitoring +if response.status_code == 401: + logger.warning(f"Auth failed for {action_type} action: {action_name}") +``` + +## Version History +- **0.235.027** - Group agent loading fix (prerequisite) +- **0.235.028** - Group action schema merging parity fix (this document) diff --git a/docs/fixes/GROUP_AGENT_LOADING_FIX.md b/docs/fixes/GROUP_AGENT_LOADING_FIX.md new file mode 100644 index 00000000..62389eb9 --- /dev/null +++ b/docs/fixes/GROUP_AGENT_LOADING_FIX.md @@ -0,0 +1,241 @@ +# Group Agent Loading Fix + +## Header Information + +**Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode +**Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. +**Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. +**Version Implemented:** 0.235.027 +**Date:** January 22, 2026 + +## Problem Statement + +### Symptoms +When a user selected a group agent in per-user semantic kernel mode: +- The agent selection would fall back to the global "researcher" agent +- Plugin count would be zero (`plugin_count: 0, plugins: []`) +- Agent would ask clarifying questions instead of executing available actions +- No group agents appeared in the available agents list +- Group actions (plugins) were not accessible even though they existed in the database + +### Impact +- **Severity:** High - Group agents completely non-functional in per-user kernel mode +- **Affected Users:** All users with per-user semantic kernel enabled who are members of groups +- **Workaround:** None - only global agents worked + +### Evidence from Logs +``` +[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] Found 2 global agents to merge +[SK Loader] After merging: 3 total agents +[DEBUG] [INFO]: [SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False)] +[DEBUG] [INFO]: [SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False +[DEBUG] [INFO]: [SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent +[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c No agent found matching user-selected agent: cio6_servicenow_test_agent +``` + +Notice: Only 3 agents loaded (2 global + 1 personal), **zero group agents** despite user being member of group "cio6". + +## Root Cause Analysis + +### Architectural Gap +The `load_user_semantic_kernel()` function in `semantic_kernel_loader.py` had the following loading sequence: + +1. ✅ Load personal agents via `get_personal_agents(user_id)` +2. ✅ Conditionally merge global agents if `merge_global_semantic_kernel_with_workspace` enabled +3. ❌ **MISSING:** Load group agents from user's group memberships +4. ✅ Load personal actions via `get_personal_actions(user_id)` +5. ✅ Conditionally merge global actions if merge enabled +6. ❌ **MISSING:** Load group actions from user's group memberships + +### Why It Was Missed +The code had logic to load a **single selected group agent** if explicitly requested, but this was: +- Only triggered when a specific group agent was pre-selected +- Required explicit group ID resolution +- Did not load **all** group agents from user's memberships +- Failed to load group agents proactively for selection + +This created a chicken-and-egg problem: the agent couldn't be selected because it wasn't loaded, and it wasn't loaded unless it was selected. + +## Technical Details + +### Files Modified +1. **`semantic_kernel_loader.py`** (Lines ~1155-1250) + - Added group agent loading after personal agents + - Added group action loading after personal actions + - Removed redundant single-agent loading logic + +2. **`config.py`** (Line 91) + - Updated VERSION from "0.235.026" to "0.235.027" + +### Code Changes + +#### Before (Pseudocode) +```python +agents_cfg = get_personal_agents(user_id) +# Mark personal agents +for agent in agents_cfg: + agent['is_global'] = False + +# Only try to load ONE selected group agent if explicitly requested +if selected_agent_is_group: + # Complex logic to find and add single group agent + +# Merge global agents if enabled +if merge_global: + # Add global agents + +# Load personal actions only +plugin_manifests = get_personal_actions(user_id) +``` + +#### After (Pseudocode) +```python +agents_cfg = get_personal_agents(user_id) +# Mark personal agents +for agent in agents_cfg: + agent['is_global'] = False + agent['is_group'] = False + +# Load ALL group agents from user's group memberships +user_groups = get_user_groups(user_id) +for group in user_groups: + group_agents = get_group_agents(group_id) + for group_agent in group_agents: + # Mark and add to agents_cfg + group_agent['is_global'] = False + group_agent['is_group'] = True + group_agent['group_id'] = group_id + group_agent['group_name'] = group_name + agents_cfg.append(group_agent) + +# Merge global agents if enabled (unchanged) +if merge_global: + # Add global agents + +# Load personal actions +plugin_manifests = get_personal_actions(user_id) + +# Load ALL group actions from user's group memberships +for group in user_groups: + group_actions = get_group_actions(group_id) + plugin_manifests.extend(group_actions) +``` + +### Key Implementation Details + +**Group Agent Loading:** +```python +from functions_group import get_user_groups +from functions_group_agents import get_group_agents + +user_groups = [] # Initialize to empty list +try: + user_groups = get_user_groups(user_id) + print(f"[SK Loader] User '{user_id}' is a member of {len(user_groups)} groups") + + group_agent_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_agents = get_group_agents(group_id) + for group_agent in group_agents: + group_agent['is_global'] = False + group_agent['is_group'] = True + group_agent['group_id'] = group_id + group_agent['group_name'] = group_name + agents_cfg.append(group_agent) + group_agent_count += 1 + print(f"[SK Loader] Loaded {len(group_agents)} agents from group '{group_name}' (id: {group_id})") + + if group_agent_count > 0: + log_event(f"[SK Loader] Loaded {group_agent_count} group agents from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) +except Exception as e: + log_event(f"[SK Loader] Error loading group agents for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) + user_groups = [] # Reset to empty on error +``` + +**Group Action Loading:** +```python +# Load group actions from all groups the user is a member of +try: + group_action_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_actions = get_group_actions(group_id, return_type=SecretReturnType.NAME) + plugin_manifests.extend(group_actions) + group_action_count += len(group_actions) + print(f"[SK Loader] Loaded {len(group_actions)} actions from group '{group_name}' (id: {group_id})") + + if group_action_count > 0: + log_event(f"[SK Loader] Loaded {group_action_count} group actions from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) +except Exception as e: + log_event(f"[SK Loader] Error loading group actions for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) +``` + +### Functions Used +- **`get_user_groups(user_id)`** - Returns all groups where user is a member (from `functions_group.py`) +- **`get_group_agents(group_id)`** - Returns all agents for a specific group (from `functions_group_agents.py`) +- **`get_group_actions(group_id, return_type)`** - Returns all actions/plugins for a specific group (from `functions_group_actions.py`) + +### Error Handling +- Both group agent and group action loading are wrapped in try-except blocks +- Errors are logged with full exception tracebacks +- On error, `user_groups` is reset to empty list to prevent downstream issues +- System gracefully degrades to personal + global agents if group loading fails + +## Validation + +### Test Scenario +1. **Setup:** + - User `f016493e-9395-4120-91b5-bac4276b6b6c` is member of group `cio6` (ID: `72254e24-4bc6-4680-bc2e-c56d5214d8e8`) + - Group has agent `cio6_servicenow_test_agent` with action `cio6_servicenow_query_incidents` + - Per-user semantic kernel mode enabled + - Global agent merging enabled + +2. **User Action:** + - User selects group agent `cio6_servicenow_test_agent` + - User submits message: "Show me all ServiceNow incidents" + +### Before Fix - Failure Behavior +``` +[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] After merging: 3 total agents # Only personal + global +[SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False +[SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent +[SK Loader] selected_agent fallback to first agent: researcher # ❌ Wrong agent +[Enhanced Agent Citations] Extracted 0 detailed plugin invocations # ❌ No actions +{'agent': 'researcher', 'plugin_count': 0} # ❌ Zero plugins +``` + +**Result:** Agent asks clarifying questions instead of querying ServiceNow. + +### After Fix - Success Behavior +``` +[SK Loader] User settings found 1 personal agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] User 'f016493e-9395-4120-91b5-bac4276b6b6c' is a member of 1 groups # ✅ Groups detected +[SK Loader] Loaded 1 agents from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group agent loaded +[SK Loader] Loaded 1 group agents from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success +[SK Loader] Total agents loaded: 2 (personal + group) for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] After merging: 4 total agents # ✅ Includes group agent +[SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False), ('cio6_servicenow_test_agent', False)] # ✅ Group agent present +[SK Loader] Loaded 1 actions from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group action loaded +[SK Loader] Loaded 1 group actions from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success +[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c Found EXACT match for agent: cio6_servicenow_test_agent (is_global=False) # ✅ Agent found +[SK Loader] Plugin cio6_servicenow_query_incidents: SUCCESS # ✅ Plugin loaded +``` + +**Result:** Correct group agent selected with its action available for execution. + +### Verification Checklist +- [x] Personal agents still load correctly +- [x] Global agents still merge correctly when enabled +- [x] Group agents load for all user's group memberships +- [x] Group actions load for all user's group memberships +- [x] Agents properly marked with `is_group` and `group_id` flags +- [x] Agent selection finds group agents by name +- [x] Error handling prevents crashes if group loading fails +- [x] Logging provides visibility into group loading process \ No newline at end of file diff --git a/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md b/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md new file mode 100644 index 00000000..34eadb4a --- /dev/null +++ b/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md @@ -0,0 +1,205 @@ +# OpenAPI Basic Authentication Fix + +**Version:** 0.235.026 +**Issue:** OpenAPI actions with Basic Authentication fail with "session not authenticated" error +**Root Cause:** Mismatch between authentication format stored by UI and format expected by OpenAPI plugin +**Status:** ✅ Fixed + +--- + +## Problem Description + +When configuring an OpenAPI action with Basic Authentication in the Simple Chat admin interface: + +1. User uploads OpenAPI spec with `securitySchemes.basicAuth` defined +2. User selects "Basic Auth" authentication type +3. User enters username and password in the configuration wizard +4. Action is saved successfully +5. **BUT**: When agent attempts to use the action, authentication fails with error: + ``` + "I'm unable to access your ServiceNow incidents because your session + is not authenticated. Please log in to your ServiceNow instance or + check your authentication credentials." + ``` + +### Symptoms +- ❌ OpenAPI actions with Basic Auth fail despite correct credentials +- ✅ Direct API calls with same credentials work correctly +- ✅ Other Simple Chat features authenticate successfully +- ❌ Error occurs even when Base URL is correctly configured + +--- + +## Root Cause Analysis + +### Authentication Storage Format (Frontend) + +The Simple Chat admin UI (`plugin_modal_stepper.js`, lines 1539-1543) stores Basic Auth credentials as: + +```javascript +auth.type = 'key'; // Basic auth is also 'key' type in the schema +const username = document.getElementById('plugin-auth-basic-username').value.trim(); +const password = document.getElementById('plugin-auth-basic-password').value.trim(); +auth.key = `${username}:${password}`; // Store as combined string +additionalFields.auth_method = 'basic'; +``` + +**Stored format:** +```json +{ + "auth": { + "type": "key", + "key": "username:password" + }, + "additionalFields": { + "auth_method": "basic" + } +} +``` + +### Authentication Expected Format (Backend) + +The OpenAPI plugin (`openapi_plugin.py`, lines 952-955) expects Basic Auth as: + +```python +elif auth_type == "basic": + import base64 + username = self.auth.get("username", "") + password = self.auth.get("password", "") + credentials = base64.b64encode(f"{username}:{password}".encode()).decode() + headers["Authorization"] = f"Basic {credentials}" +``` + +**Expected format:** +```json +{ + "auth": { + "type": "basic", + "username": "actual_username", + "password": "actual_password" + } +} +``` + +### The Mismatch + +❌ **Frontend stores:** `auth.type='key'`, `auth.key='username:password'` +❌ **Backend expects:** `auth.type='basic'`, `auth.username`, `auth.password` +❌ **Result:** Plugin code path for Basic Auth (`elif auth_type == "basic"`) never executes +❌ **Consequence:** No `Authorization` header added, API returns authentication error + +--- + +## Solution Implementation + +### Fix Location +**File:** `application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py` +**Function:** `_extract_auth_config()` +**Lines:** 129-166 + +### Code Changes + +Added authentication format transformation logic to detect and convert Simple Chat's storage format into OpenAPI plugin's expected format: + +```python +@classmethod +def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: + """Extract authentication configuration from plugin config.""" + auth_config = config.get('auth', {}) + if not auth_config: + return {} + + auth_type = auth_config.get('type', 'none') + + if auth_type == 'none': + return {} + + # Check if this is basic auth stored in the 'key' field format + # Simple Chat stores basic auth as: auth.type='key', auth.key='username:password', + # additionalFields.auth_method='basic' + additional_fields = config.get('additionalFields', {}) + auth_method = additional_fields.get('auth_method', '') + + if auth_type == 'key' and auth_method == 'basic': + # Extract username and password from the combined key + key = auth_config.get('key', '') + if ':' in key: + username, password = key.split(':', 1) + return { + 'type': 'basic', + 'username': username, + 'password': password + } + else: + # Malformed basic auth key + return {} + + # For bearer tokens stored as 'key' type + if auth_type == 'key' and auth_method == 'bearer': + return { + 'type': 'bearer', + 'token': auth_config.get('key', '') + } + + # For OAuth2 stored as 'key' type + if auth_type == 'key' and auth_method == 'oauth2': + return { + 'type': 'bearer', # OAuth2 tokens are typically bearer tokens + 'token': auth_config.get('key', '') + } + + # Return the auth config as-is for other auth types + return auth_config +``` + +### How It Works + +1. **Detection:** Check if `auth.type == 'key'` AND `additionalFields.auth_method == 'basic'` +2. **Extraction:** Split `auth.key` on first `:` to get username and password +3. **Transformation:** Return new dict with `type='basic'`, `username`, and `password` +4. **Pass-through:** OpenAPI plugin receives correct format and adds Authorization header + +### Additional Auth Method Support + +The fix also handles other authentication methods stored in the same format: +- **Bearer tokens:** `auth_method='bearer'` → transforms to `{type: 'bearer', token: ...}` +- **OAuth2:** `auth_method='oauth2'` → transforms to `{type: 'bearer', token: ...}` + +--- + +## Testing + +### Before Fix +```bash +# Test action: servicenow_query_incidents +User: "Show me all incidents in ServiceNow" +Agent: "I'm unable to access your ServiceNow incidents because your + session is not authenticated..." + +# HTTP request (no Authorization header sent): +GET https://dev222288.service-now.com/api/now/table/incident +# Response: 401 Unauthorized or session expired error +``` + +### After Fix +```bash +# Test action: servicenow_query_incidents +User: "Show me all incidents in ServiceNow" +Agent: "Here are your ServiceNow incidents: ..." + +# HTTP request (Authorization header correctly added): +GET https://dev222288.service-now.com/api/now/table/incident +Authorization: Basic + +# Response: 200 OK with incident data +``` + +### Validation Steps +1. ✅ Create OpenAPI action with Basic Auth +2. ✅ Enter username and password in admin wizard +3. ✅ Save action successfully +4. ✅ Attach action to agent +5. ✅ Test agent with prompt requiring action +6. ✅ Verify Authorization header is sent +7. ✅ Verify API returns 200 OK with data +8. ✅ Verify agent processes response correctly \ No newline at end of file diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md new file mode 100644 index 00000000..66e5a983 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -0,0 +1,762 @@ +# ServiceNow Integration Guide + +## Overview + +This guide documents the integration between Simple Chat and ServiceNow, enabling AI-powered incident management, ticket analysis, and support operations through natural language prompts. + +> **⚠️ Important - Work in Progress:** +> This integration is under active development. **Check back regularly for updates** to the OpenAPI specifications and agent instructions. Unit testing of prompts is still in progress, so further changes to the spec files and agent instruction file are expected. + +--- + +## Integration Architecture + +**Approach:** Hybrid Integration +- **ServiceNow OpenAPI Actions** - Modular API integration for CRUD operations +- **ServiceNow Support Agent** - Specialized AI agent using those actions + +--- + +## Prerequisites + +### Simple Chat Requirements +- ✅ Agents enabled (`enable_semantic_kernel: True`) +- ✅ Workspace Mode enabled (`per_user_semantic_kernel: True`) +- ✅ Global Actions enabled +- ✅ Application restarted after enabling Workspace Mode + +### ServiceNow Requirements +- [ ] ServiceNow Developer Instance (Zurich - Latest release recommended) +- [ ] Integration user with API access +- [ ] API credentials (Basic Auth or OAuth 2.0) + +--- + +## Phase 1: ServiceNow Instance Setup + +### Step 1: Request Developer Instance + +1. Navigate to: https://developer.servicenow.com/ +2. Click "Request an Instance" +3. Select **Zurich (Latest release)** +4. Click "Request" +5. Wait for instance provisioning (typically 2-5 minutes) + +**You'll receive:** +``` +Instance URL: https://devXXXXX.service-now.com +Admin Username: admin +Admin Password: [provided by ServiceNow] +``` + +### Step 2: Create Integration User + +> **Note:** This step demonstrates basic authentication setup for initial testing. For production deployments using Bearer Token authentication, refer to "SERVICENOW_OAUTH_SETUP.md". + +1. Log into your ServiceNow instance as admin +2. Navigate to: **User Administration** → **Users** +3. Click **New** to create integration user: + ``` + Username: simplechat6_integration + First Name: Simple + Last Name: Chat Integration + Email: [your email] + Time Zone: [your timezone] + ``` + +4. Assign Roles: + - Navigate to **Roles** tab + - Add roles: + - `rest_api_explorer` - For REST API access + - `itil` - For incident management + - `knowledge` - For knowledge base access (optional) + +5. Set Password: + - Click **Set Password** + - Create secure password + - Save for later use + +### Step 3: Test REST API Access + +1. Navigate to: **System Web Services** → **REST** → **REST API Explorer** +2. URL: `https://devXXXXX.service-now.com/$restapi.do` +3. Select API: **Table API** +4. Select Table: **incident** +5. Click **Send** to test query +6. Verify you get JSON response with incident data + +**Example successful response:** +```json +{ + "result": [ + { + "number": "INC0000001", + "short_description": "Test incident", + "state": "1" + } + ] +} +``` + +--- + +## Phase 2: OpenAPI Specification + +### ServiceNow API Endpoints + +The integration uses two OpenAPI specification files that define all ServiceNow REST API operations: + +#### 1. Incident Management API +**Files:** +- **Bearer Token Auth:** `sample_servicenow_incident_api.yaml` (Recommended for production) +- **Basic Auth:** `sample_servicenow_incident_api_basicauth.yaml` (For testing only) + +**Base URL:** `https://devXXXXX.service-now.com/api/now` + +**Endpoints:** +- `GET /table/incident` - Query incidents with filters +- `POST /table/incident` - Create new incident +- `GET /table/incident/{sys_id}` - Get specific incident details +- `PATCH /table/incident/{sys_id}` - Update incident +- `GET /stats/incident` - Get incident statistics and aggregations + +**Operations:** +- `queryIncidents` - Query incidents based on filters (state, priority, date range, etc.) +- `createIncident` - Create new incident with short_description, description, priority, etc. +- `getIncidentDetails` - Retrieve full details of specific incident by sys_id +- `updateIncident` - Update incident fields (state, work_notes, priority, assigned_to, etc.) +- `getIncidentStats` - Get aggregated statistics (count, averages, grouping by fields) + +#### 2. Knowledge Base API +**Files:** +- **Bearer Token Auth:** `sample_now_knowledge_latest_spec.yaml` (Recommended for production) +- **Basic Auth:** `sample_now_knowledge_latest_spec_basicauth.yaml` (For testing only) + +**Base URL:** `https://devXXXXX.service-now.com` + +**Endpoints:** +- `GET /api/now/table/kb_knowledge` - Search knowledge base articles +- `GET /api/now/table/kb_knowledge/{sys_id}` - Get specific article details + +**Operations:** +- `searchKnowledgeFacets` - Search knowledge articles with progressive fallback strategy +- `getKnowledgeArticle` - Retrieve full content of specific knowledge article + +### OpenAPI Specification Files + +**Locations:** `docs/how-to/agents/ServiceNow/open_api_specs/` + +**Available Authentication Options:** + +#### Bearer Token Authentication (Production) +- `sample_servicenow_incident_api.yaml` - Incident management with OAuth 2.0 bearer token +- `sample_now_knowledge_latest_spec.yaml` - Knowledge base search with OAuth 2.0 bearer token +- **Use these for:** Production deployments, secure enterprise environments +- **Setup guide:** See `SERVICENOW_OAUTH_SETUP.md` for OAuth configuration + +#### Basic Authentication (Testing Only) +- `sample_servicenow_incident_api_basicauth.yaml` - Incident management with username:password +- `sample_now_knowledge_latest_spec_basicauth.yaml` - Knowledge base search with username:password +- **Use these for:** Initial testing, development instances, proof of concept +- **Security note:** Not recommended for production use + +**Status:** ✅ Created and configured + +**Key Features:** +- ✅ Both authentication methods supported (bearer token and basic auth) +- ✅ Comprehensive parameter documentation with detailed descriptions +- ✅ Critical usage patterns documented: + - Progressive search strategy (fallback from exact phrase to broad keyword) + - sys_id requirements and query-first patterns for updates + - Field mapping for create/update operations + - Work notes timing considerations (updates may take a few moments to appear) +- ✅ Query examples and common use case patterns +- ✅ Field descriptions, constraints, and validation rules +- ✅ State/priority/urgency enumerations documented +- ✅ Error handling guidance and status codes +- ✅ Pagination and filtering parameter examples +- ✅ ServiceNow-specific query syntax (encoded queries, operators) + +> **⚠️ Important:** These OpenAPI specifications are continuously tested and refined based on real-world use cases, agent behavior analysis, and production feedback. Regular updates ensure optimal AI agent understanding and reliable API interactions. + +--- + +## Phase 3: Simple Chat Configuration + +### Step 1: Add ServiceNow Actions + +> **Note:** This integration uses **two separate actions** because ServiceNow has distinct API endpoints for incident management and knowledge base operations, each with its own OpenAPI specification file. + +1. Navigate to: **Admin Settings** → **Actions Configuration** +2. Click **"Add Action"** +3. **Select Action Type: OpenAPI** + - ServiceNow REST APIs use OpenAPI/Swagger specifications + - OpenAPI type supports: External API integration, HTTP/HTTPS requests, authentication, JSON payloads + - Click **"Next"** after selecting OpenAPI + +#### Action 1: Incident Management +``` +Name: servicenow_manage_incident +Display Name: ServiceNow - Manage Incidents +Type: OpenAPI +Description: Complete incident management - query, create, update, retrieve details, and get statistics +OpenAPI Spec: [Upload sample_servicenow_incident_api.yaml or sample_servicenow_incident_api_basicauth.yaml] +Base URL: https://devXXXXX.service-now.com + +Operations Included: + - queryIncidents: Query/filter incidents with advanced search + - createIncident: Create new incidents with all fields + - getIncidentDetails: Retrieve full incident details by sys_id + - updateIncident: Update incident state, assignments, work notes, etc. + - getIncidentStats: Get aggregated statistics and metrics + +Authentication Options: + +Option A - Basic Auth (Testing Only): + Auth Type: key + Key: username:password (or use Key Vault reference) + OpenAPI Spec File: sample_servicenow_incident_api_basicauth.yaml + +Option B - OAuth Bearer Token (Recommended for Production): + Auth Type: key + Key: (or use Key Vault reference) + OpenAPI Spec File: sample_servicenow_incident_api.yaml + See: SERVICENOW_OAUTH_SETUP.md for OAuth setup + +Scope: Global or Group +``` + +**Repeat Step 1 for Knowledge Base Action:** + +#### Action 2: Knowledge Base Search (Optional) +``` +Name: servicenow_search_knowledge_base +Display Name: ServiceNow - Search Knowledge Base +Type: OpenAPI +Description: Search knowledge articles with progressive fallback and retrieve full article content +OpenAPI Spec: [Upload sample_now_knowledge_latest_spec.yaml or sample_now_knowledge_latest_spec_basicauth.yaml] +Base URL: https://devXXXXX.service-now.com + +Operations Included: + - searchKnowledgeFacets: Search KB articles with progressive search strategy + - getKnowledgeArticle: Retrieve complete article content by sys_id + +[Same auth config as above] + +Scope: Global or Group +``` + +> **💡 Tip:** If you only need incident management without knowledge base search, you can skip Action 2 and configure your agent with only the `servicenow_manage_incident` action. + +--- + +## Phase 4: Configure ServiceNow Agent + +### Step 1: Create Agent + +1. Navigate to: **Admin Settings** → **Agents Configuration** +2. Click **"Add Agent"** +3. Configure agent: + +``` +Name: servicenow_support_agent +Display Name: ServiceNow Support Agent +Description: AI agent for ServiceNow incident management and knowledge base operations + +Instructions: [Copy from servicenow_agent_instructions.txt] + +Model: gpt-4o (or your preferred model) +Scope: Global or Group +``` + +> **📄 Agent Instructions File:** +> - **Location:** `docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt` +> - **Purpose:** Comprehensive behavioral instructions for the ServiceNow support agent +> - **Usage:** Copy the entire content from this file into the "Instructions" field when creating the agent +> +> **⚠️ Important:** These instructions are continuously refined and tuned based on real-world use cases, agent behavior analysis, and production feedback. The file serves as a living reference that should be updated as new patterns emerge or edge cases are discovered. Regular review and updates ensure optimal agent performance and reliable ServiceNow interactions. + +### Step 2: Attach Actions to Agent + +1. Edit the ServiceNow Support Agent +2. Navigate to **Actions** tab +3. Select and attach: + - ✅ servicenow_manage_incident + - ✅ servicenow_search_knowledge_base +4. Save agent configuration + +--- + +## Testing the Integration + +### Test 1: Query Incidents +``` +Prompt: "Show me open critical incidents created in the last 7 days" +``` + +Expected: Agent queries incidents with appropriate filters and displays results in table format. + +### Test 2: Create Incident +``` +Prompt: "Create an incident: Email server down for Finance team, priority High, assigned to John Doe" +``` + +Expected: Agent creates incident and returns incident number. + +### Test 3: Update Incident +``` +Prompt: "Update INC0010095 - add work note: Investigating email server logs" +``` + +Expected: Agent queries for sys_id, then updates with work note. + +### Test 4: Search Knowledge Base +``` +Prompt: "Find KB articles about email troubleshooting" +``` + +Expected: Agent searches KB and returns relevant articles with links. + +--- + +## Phase 5: Testing + +### Test Scenarios + +#### Test 1: Query Recent Tickets +**Prompt:** +``` +Show me all incidents created in the last 7 days +``` + +**Expected Behavior:** +- Agent uses servicenow_query_incidents action +- Filters by created_date >= 7 days ago +- Returns formatted table with results + +**Status:** [ ] Tested + +--- + +#### Test 2: Create New Ticket +**Prompt:** +``` +Create a new incident: +- Description: Email server not responding for Finance department +- Urgency: High +- Priority: 2 +- Category: Email +``` + +**Expected Behavior:** +- Agent confirms parameters +- Uses servicenow_create_incident action +- Returns new incident number (e.g., INC0010001) + +**Status:** [ ] Tested + +--- + +#### Test 3: Trend Analysis +**Prompt:** +``` +What are the top 5 trending issues over the last 30 days? +Show incident counts for each category. +``` + +**Expected Behavior:** +- Agent queries incidents from last 30 days +- Groups by category +- Counts incidents per category +- Returns top 5 in table format + +**Status:** [ ] Tested + +--- + +#### Test 4: Support Team Analytics +**Prompt:** +``` +Who is the most active support person in the last 30 days? +Show number of tickets resolved and average resolution time. +``` + +**Expected Behavior:** +- Agent queries incidents with resolved status +- Groups by assigned_to +- Calculates counts and averages +- Returns ranked list + +**Status:** [ ] Tested + +--- + +#### Test 5: Predictive Analysis +**Prompt:** +``` +Analyze resolution entries over the last year and identify patterns. +What are the most common types of outages? +``` + +**Expected Behavior:** +- Agent queries historical data (1 year) +- Analyzes resolution notes and categories +- Identifies recurring patterns +- Provides recommendations + +**Status:** [ ] Tested + +--- + +## Security Best Practices + +### Credential Management + +**Option 1: Direct Password Entry (Quick Setup)** +- Enter ServiceNow password directly in action configuration +- Stored encrypted in Cosmos DB +- ⚠️ Less secure for production use + +**Option 2: Azure Key Vault (Recommended)** +1. Store ServiceNow credentials in Azure Key Vault +2. Create secret: `servicenow-integration-password` +3. Reference in action config: `@keyvault:servicenow-integration-password` +4. Simple Chat automatically retrieves from Key Vault + +**Status:** [ ] Credentials secured + +### API User Permissions + +**Least Privilege Principle:** +- [ ] Integration user has only required roles +- [ ] Read-only access for query actions +- [ ] Write access only for create/update actions +- [ ] No admin privileges + +### Audit Logging + +**Enable in ServiceNow:** +1. Navigate to **System Logs** → **System Log** → **REST Messages** +2. Enable logging for API calls +3. Monitor for unusual activity + +**Status:** [ ] Audit logging configured + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: "Session not authenticated" or "Session expired" error + +**Status:** ✅ **FIXED in version 0.235.026** + +**Symptoms:** +- Agent responds: "I'm unable to access your ServiceNow incidents because your session is not authenticated" +- Direct API calls with same credentials work correctly +- Base URL is configured correctly + +**Root Cause:** +This issue was caused by a mismatch between how the Simple Chat UI stores Basic Auth credentials (as `username:password` in a single field) and how the OpenAPI plugin expected them (as separate `username` and `password` fields). + +**Solution:** +The fix is included in Simple Chat v0.235.026+. The OpenAPI plugin factory now automatically transforms the authentication format when loading actions, so no user action is required. + +**For detailed technical information, see:** `docs/explanation/fixes/OPENAPI_BASIC_AUTH_FIX.md` + +#### Issue: "Authentication failed" error +**Solution:** +- Verify username (simplechat6_integration) and password are correct +- Check integration user is active +- Confirm user has `rest_api_explorer` role +- Test credentials in REST API Explorer first +- Ensure Base URL is correct: `https://devXXXXX.service-now.com/api/now` + +#### Issue: "No results returned" for queries +**Solution:** +- Check date filters are correct +- Verify table name is correct (incident, not incidents) +- Test query in ServiceNow REST API Explorer +- Check sysparm_query encoding + +#### Issue: Agent not using ServiceNow actions +**Solution:** +- Verify actions are attached to agent +- Check actions are saved as "Global" scope +- Restart application after configuration changes +- Review agent instructions for clarity + +#### Issue: "Rate limit exceeded" error +**Solution:** +- ServiceNow limits API calls per hour +- Developer instances: ~10,000 calls/hour +- Add delays between bulk operations +- Implement retry logic with exponential backoff + +--- + +## Next Steps + +### Completed +- [x] Understand integration approach +- [x] Choose ServiceNow instance (Zurich) + +### In Progress +- [ ] Request ServiceNow developer instance +- [ ] Create integration user +- [ ] Test REST API access + +### To Do +- [ ] Create OpenAPI specification +- [ ] Add ServiceNow actions in Simple Chat +- [ ] Create ServiceNow support agent +- [ ] Test all use cases +- [ ] Secure credentials with Key Vault +- [ ] Deploy to production + +--- + +## Resources + +### ServiceNow Documentation +- REST API Reference: https://developer.servicenow.com/dev.do#!/reference/api/latest/rest +- Table API Guide: https://docs.servicenow.com/bundle/latest/page/integrate/inbound-rest/concept/c_TableAPI.html +- Developer Portal: https://developer.servicenow.com/ + +### Simple Chat Documentation +- Actions Configuration: `docs/admin_configuration.md` +- Agent Creation: `docs/features.md` +- API Integration: `docs/explanation/features/` + +--- + +## Appendix + +### ServiceNow Query Syntax Examples + +**Last 7 days:** +``` +sysparm_query=sys_created_onONLast 7 days@javascript:gs.daysAgoStart(7) +``` + +**By priority:** +``` +sysparm_query=priority=1 +``` + +**By state (resolved):** +``` +sysparm_query=state=6 +``` + +**Combined filters:** +``` +sysparm_query=priority=1^state=1^sys_created_onONLast 30 days@javascript:gs.daysAgoStart(30) +``` + +### Sample Prompts for ServiceNow Actions + +Use these prompts with the ServiceNow Support Agent to test and demonstrate functionality: + +#### Query Incidents (servicenow_query_incidents) + +**Basic queries:** +- "Show me all open incidents" +- "List incidents created in the last 7 days" +- "What incidents are currently in progress?" +- "Show me all critical priority incidents" +- "Find all incidents assigned to the Finance department" + +**Advanced queries:** +- "Show me high priority incidents from last month that are still unresolved" +- "List all email-related incidents created in the last 2 weeks" +- "What incidents were opened yesterday with priority 1 or 2?" +- "Find all network incidents assigned to IT Support team" +- "Show me the most recent 20 incidents sorted by creation date" + +**Analytics and trends:** +- "What are the top 10 most common incident categories this month?" +- "How many incidents were created each day last week?" +- "Show me incident volume by priority for the last 30 days" +- "What's the average resolution time for critical incidents?" +- "Which category has the most unresolved incidents?" + +#### Create Incident (servicenow_create_incident) + +**Simple creation:** +- "Create a new incident: Email server is down for Marketing team" +- "Log a ticket: Users can't access the VPN, high urgency" +- "Open an incident for printer not working in conference room A" + +**Detailed creation:** +- "Create a critical incident: Database server crashed, all users affected, need immediate attention" +- "Log a new ticket with the following details: + - Description: Password reset portal showing error 500 + - Priority: High + - Category: Security + - Urgency: High + - Impact: Medium" + +**Template-based:** +- "Create an email server outage incident with high priority" +- "Open a standard network connectivity ticket for Building 2, Floor 3" +- "Log a hardware failure incident for laptop replacement" + +#### Get Incident Details (servicenow_get_incident) + +**By incident number:** +- "Show me details for incident INC0010001" +- "What's the status of ticket INC0010025?" +- "Get full details for incident INC0000157" +- "Show me the complete information for INC0010010" + +**Follow-up queries:** +- "What's the current status of the email server incident we created earlier?" +- "Show me all the work notes for incident INC0010005" +- "Has incident INC0010015 been assigned to anyone yet?" +- "When was INC0010020 last updated?" + +#### Update Incident (servicenow_update_incident) + +**Status updates:** +- "Mark incident INC0010001 as resolved" +- "Update INC0010025 status to In Progress" +- "Close incident INC0010005 with resolution: Issue resolved by restarting service" +- "Put incident INC0010010 on hold" + +**Assignment updates:** +- "Assign incident INC0010001 to John Smith" +- "Reassign INC0010025 to the Network Support team" +- "Change the assigned user for INC0010005" + +**Work notes:** +- "Add work note to INC0010001: Investigating email server logs, found connection timeout" +- "Update INC0010025 with note: Contacted vendor for support" +- "Add comment to INC0010010: Waiting for user response" + +**Priority changes:** +- "Increase priority of INC0010001 to Critical" +- "Lower the urgency of INC0010025 to Medium" +- "Change INC0010005 priority to 2" + +#### Get Statistics (servicenow_get_stats) + +**Volume metrics:** +- "How many incidents were created last month?" +- "What's the total incident count by category for this year?" +- "Show me incident volume trends for the last 6 months" +- "How many critical incidents were opened this week?" + +**Performance metrics:** +- "What's the average resolution time for incidents last month?" +- "Show me the mean time to resolve by category" +- "What percentage of incidents are resolved within SLA?" +- "Calculate the average time to first response" + +**Team analytics:** +- "Show me incident counts by assigned user for last 30 days" +- "Which support team resolved the most incidents this quarter?" +- "What's the workload distribution across support groups?" +- "Who has the fastest average resolution time?" + +**Categorical analysis:** +- "Break down incident counts by priority for last month" +- "Show me the distribution of incidents by state" +- "What categories have the highest incident volume?" +- "Compare email vs network incident counts for Q4" + +#### Search Knowledge Base (servicenow_search_kb) + +**Solution searches:** +- "Search the knowledge base for email configuration guides" +- "Find articles about VPN connection troubleshooting" +- "Look up password reset procedures in the KB" +- "Search for solutions to 'server not responding' errors" + +**Category searches:** +- "Show me all knowledge articles in the Email category" +- "Find network troubleshooting guides" +- "List all hardware setup articles" +- "Show me security-related KB articles" + +**Problem-specific:** +- "Find KB articles about printer connectivity issues" +- "Search for documentation on how to reset user passwords" +- "Look up articles about 'Error 500' messages" +- "Find guides for setting up mobile email access" + +**Recent/popular:** +- "What are the most viewed knowledge articles this month?" +- "Show me recently updated KB articles" +- "Find the top 10 most helpful articles" +- "List new knowledge articles from the last 30 days" + +#### Complex Multi-Action Workflows + +**Incident creation with KB lookup:** +- "Users are reporting email server issues. Search the knowledge base for solutions and if none exist, create a new incident." + +**Trend analysis with knowledge suggestions:** +- "What are the top 5 recurring issues this month? For each, suggest relevant knowledge articles." + +**Incident lifecycle:** +- "Show me all unresolved incidents from last week. For those older than 5 days, add a work note asking for status update." + +**Support quality check:** +- "Find all incidents closed yesterday. Check if resolution notes reference knowledge articles. Report which ones are missing KB references." + +**Proactive support:** +- "Analyze incidents from the last 90 days. Identify the top 3 issues that don't have knowledge articles, and suggest creating documentation for them." + +#### Natural Language Queries (Advanced Agent Capabilities) + +- "I need help with the laptop that won't connect to WiFi" + - Agent creates incident with user's details + - Searches KB for WiFi troubleshooting + - Provides step-by-step guide + - Tracks incident until resolved + +- "Show me everything related to the email outage last Tuesday" + - Agent queries incidents from that date + - Filters by email category + - Shows timeline of events + - Provides resolution summary + +- "Create a monthly support report for my manager" + - Agent gathers statistics for last month + - Calculates key metrics (volume, resolution time, SLA) + - Identifies trends and patterns + - Formats professional summary + +- "What's our biggest support challenge right now?" + - Agent analyzes recent incident data + - Identifies high-volume categories + - Calculates resolution times + - Highlights recurring problems + - Suggests improvements + +--- + +**Tip:** Start with simple queries to verify actions are working, then progress to more complex multi-action workflows. The ServiceNow Support Agent can combine multiple actions intelligently based on your natural language requests. + +### Useful ServiceNow Fields + +**Incident Table Fields:** +- `number` - Incident number (INC0000001) +- `short_description` - Brief title +- `description` - Detailed description +- `priority` - 1-5 (1=Critical, 5=Planning) +- `urgency` - 1-3 (1=High, 3=Low) +- `state` - 1=New, 2=In Progress, 6=Resolved, 7=Closed +- `assigned_to` - Assigned user +- `category` - Incident category +- `sys_created_on` - Created timestamp +- `sys_updated_on` - Updated timestamp +- `resolved_at` - Resolution timestamp +- `sys_id` - Unique identifier + +--- + +**Last Updated:** January 21, 2026 +**Status:** Initial Draft - In Progress diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md new file mode 100644 index 00000000..02646450 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md @@ -0,0 +1,503 @@ +# ServiceNow OAuth 2.0 Setup for Simple Chat + +## Overview +This guide shows you how to configure OAuth 2.0 bearer token authentication for ServiceNow integration with Simple Chat using the **modern "New Inbound Integration Experience"** method. This is more secure than Basic Auth and recommended for production environments. + +> **Note:** This guide uses the current ServiceNow OAuth configuration method. The deprecated "Create an OAuth API endpoint for external clients" method is no longer recommended. + +## Prerequisites +- ServiceNow instance (Developer or Production) +- Admin access to ServiceNow +- **ServiceNow integration user** with appropriate roles (e.g., `itil`, `incident_manager`) + - **Best Practice**: Create a dedicated user (e.g., `simplechat_integration`) instead of using a personal account + - This user's permissions determine what the OAuth token can access +- Existing Simple Chat ServiceNow action (or create new one) + +--- + +## Part 1: Configure OAuth in ServiceNow + +### Step 1: Create ServiceNow Integration User (Recommended) + +Before creating the OAuth application, create a dedicated integration user: + +1. **Navigate to User Administration:** + - In ServiceNow, search for: **"Users"** in the left navigation filter + - Click: **System Security > Users** + +2. **Create New User:** + - Click **"New"** + - Fill in the form: + ``` + User ID: simplechat_integration + First name: Simple Chat + Last name: Integration + Email: your-email@example.com + Password: [Set a strong password - save this!] + ``` + +3. **Assign Roles:** + - Click on the **Roles** tab + - Add appropriate roles based on your Simple Chat use case: + - `itil` - Read access to ITIL tables (incidents, problems, changes) + - `incident_manager` - Create and update incidents + - `knowledge` - Read knowledge base articles + - **Security Best Practice**: Grant **only the minimum roles** needed for Simple Chat operations + +4. **Activate and Save:** + - Check the **"Active"** checkbox + - Click **"Submit"** + +5. **Save These Credentials (you'll need them in Step 3):** + ``` + ServiceNow Integration User + Username: simplechat_integration + Password: [the password you set] + ``` + +> **Why Create a Dedicated User?** +> - ✅ **Security**: Limit blast radius if credentials are compromised +> - ✅ **Audit Trail**: Clear visibility in ServiceNow logs (shows "simplechat_integration" performed actions) +> - ✅ **Permission Control**: Grant only the specific roles needed, not your full admin rights +> - ✅ **Lifecycle Management**: Can deactivate or rotate credentials without affecting personal accounts + +--- + +### Step 2: Create OAuth Application + +1. **Log in to your ServiceNow instance** as an admin + - URL: `https://devnnnnnn.service-now.com` + +2. **Navigate to OAuth Application Registry:** +2. **Navigate to OAuth Application Registry:** + ``` + System OAuth > Application Registry + ``` + Or search for "OAuth" in the navigation filter + +3. **Create New OAuth Integration:** + - Click **New** + - Select **"New Inbound Integration Experience"** (recommended for external clients) + - ⚠️ **Do NOT use** the deprecated "Create an OAuth API endpoint for external clients" + +4. **Select OAuth Grant Type:** + + ServiceNow will present you with several OAuth grant type options: + + **For this POC, select: "OAuth - Resource owner password credential grant"** + + > **📋 Why This Grant Type for POC:** + > - ✅ **Trusted application scenario**: Simple Chat is a trusted first-party application on your Azure infrastructure + > - ✅ **User context preserved**: Actions execute with the **integration user's permissions** and audit trail + > - Token request requires: **OAuth app credentials** (Client ID/Secret) + **ServiceNow user credentials** (Username/Password) + > - ServiceNow issues token **on behalf of that specific user** + > - All API calls execute with that user's roles, ACLs, and permissions + > - Audit logs show the integration user's name, not just "OAuth app" + > - ✅ **Simple token management**: Easy to obtain and refresh tokens programmatically + > - ✅ **Development/testing friendly**: Works well for POC without complex OAuth flows + > - ✅ **Server-to-server integration**: Simple Chat backend directly requests tokens using credentials + + > **⚠️ IMPORTANT - Review Grant Type for Production:** + > + > The OAuth grant type should be **revisited based on your customer's security requirements** and deployment scenario: + > + > | Grant Type | Best For | Use When | + > |------------|----------|----------| + > | **Resource Owner Password** | Trusted apps, POC/Dev | App is first-party, trusted infrastructure, need user context | + > | **Client Credentials** | Machine-to-machine | No user context needed, service account only | + > | **Authorization Code** | Third-party apps | Interactive user consent required, multi-tenant scenarios | + > | **JWT Bearer** | Advanced scenarios | Token exchange, federated identity, microservices | + > + > **Production Considerations:** + > - If customer requires **no password storage**, use Authorization Code grant with PKCE + > - If customer requires **service account only**, use Client Credentials grant + > - If customer has **strict OAuth compliance**, avoid Resource Owner Password grant (considered legacy by some standards) + > - If integrating with **external identity providers**, use JWT Bearer or Authorization Code grant + > + > Always align the grant type choice with your customer's security policies and compliance requirements. + +5. **Configure the Integration Form:** + + ServiceNow presents a "New record" form with several sections. Configure as follows: + + **Details Section:** + ``` + Name: Simple Chat Integration + Provider name: Azure app service (auto-filled) + Client ID: (auto-generated - COPY THIS!) + Client secret: (auto-generated - COPY THIS IMMEDIATELY!) + Comments: OAuth integration for Simple Chat AI assistant + Active: ☑ Checked + ``` + + **Auth Scope Section:** + ``` + Auth scope: useraccount (default) + Limit authorization to following APIs: (leave empty for POC) + ``` + > ⚠️ The "useraccount" scope grants access to all resources available to the signed-in user. This is acceptable for POC with a dedicated integration user account. For production, consider creating custom scopes to limit access to only required APIs. + + **Advanced Options (optional):** + ``` + Enforce token restriction: ☐ Unchecked (for POC) + Token Format: Opaque (default) + Access token lifespan (seconds): 3600 (1 hour - recommended for POC) + Refresh token lifespan (seconds): 86400 (24 hours - recommended for POC) + ``` + > **Note:** ServiceNow defaults to 1800 seconds (30 min) for access tokens, which is too short for testing. Change to longer based on your needs or the dev/testing duration. + +6. **⚠️ CRITICAL - Copy Credentials BEFORE Saving:** + + **Before clicking "Save", you MUST copy these values:** + + 1. **Client ID:** Visible in plain text (e.g., `565d53a80dfe4cb89b8869fd1d977308`) + - Select and copy the entire value + + 2. **Client Secret:** Hidden behind dots + - Click the 👁️ (eye icon) to reveal, OR + - Click the 📋 (copy icon) to copy directly + - **This may only be shown once - copy it now!** + + **Save these values securely** - paste them into a text file or password manager immediately. + + Example format to save: + ``` + ServiceNow OAuth Credentials + Instance: https://devnnnnnn.service-now.com + Client ID: 565d53a... + Client Secret: [paste the revealed secret here] + Token Endpoint: https://devnnnnnn.service-now.com/oauth_token.do + Username: + ``` + +7. **Click "Save"** + +8. **Note the token endpoint:** + - Token endpoint: `https://devnnnnnn.service-now.com/oauth_token.do` + +--- + +### Step 3: Obtain Access Token + +You have two options to get an access token. + +> **Important:** The token request requires **BOTH**: +> - **OAuth App Credentials**: `client_id` and `client_secret` (from Step 1) +> - **ServiceNow User Credentials**: `username` and `password` (integration user you created) +> +> The resulting token will execute API calls **as that integration user** with their specific roles and permissions. + +#### **Option A: Using REST Client (Postman/Curl)** + +**Request:** +```bash +curl -X POST https://devnnnnnn.service-now.com/oauth_token.do \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=YOUR_CLIENT_ID" \ # OAuth app Client ID + -d "client_secret=YOUR_CLIENT_SECRET" \ # OAuth app Client Secret + -d "username=YOUR_USERNAME" \ # ServiceNow integration user + -d "password=YOUR_PASSWORD" # Integration user's password +``` + +**Response:** +```json +{ + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "scope": "useraccount", + "token_type": "Bearer", + "expires_in": 31536000 +} +``` + +#### **Option B: Using Python Script** + +Create `get_servicenow_token.py`: +```python +#!/usr/bin/env python3 +""" +Get ServiceNow OAuth access token for Simple Chat integration. + +Requires BOTH: +- OAuth App credentials (Client ID/Secret from ServiceNow OAuth registry) +- ServiceNow integration user credentials (Username/Password) + +The token will execute API calls as the integration user with their permissions. +""" + +import requests +import json + +# ServiceNow OAuth App credentials (from Step 1 - OAuth registry) +SERVICENOW_INSTANCE = "https://devnnnnnn.service-now.com" +CLIENT_ID = "YOUR_CLIENT_ID" # From OAuth Application Registry +CLIENT_SECRET = "YOUR_CLIENT_SECRET" # From OAuth Application Registry + +# ServiceNow integration user credentials (dedicated user with specific roles) +USERNAME = "YOUR_USERNAME" # e.g., simplechat_integration +PASSWORD = "YOUR_PASSWORD" # Integration user's password + +def get_access_token(): + """Get OAuth access token from ServiceNow.""" + url = f"{SERVICENOW_INSTANCE}/oauth_token.do" + + data = { + 'grant_type': 'password', + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'username': USERNAME, + 'password': PASSWORD + } + + response = requests.post(url, data=data) + + if response.status_code == 200: + token_data = response.json() + print("✅ Access Token obtained successfully!") + print(f"\nAccess Token: {token_data['access_token']}") + print(f"Expires in: {token_data['expires_in']} seconds") + print(f"Token Type: {token_data['token_type']}") + + # Save to file + with open('servicenow_token.json', 'w') as f: + json.dump(token_data, f, indent=2) + print("\n📁 Token saved to servicenow_token.json") + + return token_data['access_token'] + else: + print(f"❌ Failed to get token: {response.status_code}") + print(response.text) + return None + +if __name__ == "__main__": + get_access_token() +``` + +Run the script: +```bash +python get_servicenow_token.py +``` + +--- + +## Part 2: Configure Action in Simple Chat + +### Step 1: Navigate to Actions Configuration + +1. **Navigate to Actions page:** + - Go to **Settings** > **Actions** (Global) or **Group Settings** > **Actions** (Group-specific) + +2. **Edit your ServiceNow action** or click **"Create New Action"** + +### Step 2: Upload OpenAPI Specification + +1. **Action Details:** + ``` + Name: ServiceNow Query Incidents (or your preferred name) + Display Name: ServiceNow - Query Incident + Description: Query ServiceNow incidents with filters + ``` + +2. **OpenAPI Specification:** + - Upload your `servicenow_incident_api.yaml` file + - Or paste the OpenAPI spec content directly + +3. **Base URL:** + ``` + https://devnnnnnn.service-now.com/api/now + ``` + *(Replace devnnnnnn with your actual ServiceNow instance)* + +### Step 3: Configure Bearer Token Authentication + +In the **Authentication Configuration** section: + +1. **Select Authentication Type:** + - From the **"Type"** dropdown, select: **Bearer Token** + +2. **Enter Token:** + - Paste your access token in the **"Token"** field + ``` + YOUR_ACCESS_TOKEN_FROM_PART_1_STEP_2 + ``` + *(Use the actual token obtained from Part 1, Step 2)* + +3. **Save the Action** + +**That's it!** Simple Chat will automatically: +- Add `Authorization: Bearer YOUR_TOKEN` header to all requests +- Handle the token properly for ServiceNow API authentication + +> **Production Considerations:** +> +> For production deployments, consider the following: +> +> 1. **Secure Token Storage**: Store the OAuth token in Azure Key Vault rather than directly in the action configuration +> - Enables centralized secret management and rotation +> - Provides audit logging for secret access +> - Allows token updates without modifying Simple Chat configuration +> +> 2. **Token Expiration Management**: OAuth tokens have limited lifespans (typically 1-8 hours) +> - **Monitor token expiration**: Set up alerts before tokens expire +> - **Implement token refresh**: Use the refresh token to obtain new access tokens automatically +> - **Automated renewal**: Consider creating an Azure Function or scheduled task to refresh tokens periodically +> - See the "Token Refresh Strategy" section below for implementation options +> +> 3. **Graceful Failure Handling**: Implement monitoring to detect authentication failures due to expired tokens + +--- + +## Part 3: Testing + +### Test with Simple Chat Agent + +1. **Open Simple Chat** and select your ServiceNow agent +2. **Test query:** + ``` + Show me recent incidents + ``` + +3. **Check logs** for successful authentication: + ``` + Added bearer auth: eyJ0eXAi... + Authorization: Bearer eyJ0eXAi... + ``` + +### Test with Curl + +```bash +curl -X GET \ + "https://devnnnnnn.service-now.com/api/now/table/incident?sysparm_limit=5" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Accept: application/json" +``` + +--- + +## Token Refresh Strategy + +OAuth tokens expire. Here are your options: + +### Option 1: Manual Refresh (Simple) +- Set calendar reminder before token expires +- Run `get_servicenow_token.py` script +- Update Key Vault secret +- Simple Chat will use new token automatically + +### Option 2: Automatic Refresh (Advanced) +Create a scheduled task/Azure Function to refresh tokens: + +```python +# refresh_servicenow_token.py +import requests +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential + +def refresh_token(): + # Get refresh token from Key Vault + credential = DefaultAzureCredential() + client = SecretClient(vault_url="https://your-vault.vault.azure.net", credential=credential) + refresh_token = client.get_secret("servicenow-refresh-token").value + + # Request new access token + response = requests.post( + "https://devnnnnnn.service-now.com/oauth_token.do", + data={ + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token, + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + } + ) + + if response.status_code == 200: + new_token = response.json()['access_token'] + + # Update Key Vault + client.set_secret("servicenow-oauth-token", new_token) + print("✅ Token refreshed successfully!") + else: + print(f"❌ Refresh failed: {response.text}") + +if __name__ == "__main__": + refresh_token() +``` + +Schedule with Azure Function (Timer Trigger): +``` +Trigger: Every 50 minutes (before 1-hour expiration) +``` + +### Option 3: Long-Lived Tokens +Configure longer token lifespans in ServiceNow: +``` +Access Token Lifespan: 28800 (8 hours) +Refresh Token Lifespan: 604800 (7 days) +``` + +--- + +## Security Best Practices + +### ✅ DO: +- Store tokens in Azure Key Vault +- Use HTTPS for all requests +- Set appropriate token expiration times +- Rotate tokens regularly +- Use refresh tokens to avoid storing passwords +- Monitor token usage in ServiceNow + +### ❌ DON'T: +- Hardcode tokens in code +- Share tokens between environments +- Use overly long token lifespans +- Commit tokens to source control +- Use the same credentials for dev and prod + +--- + +## Comparison: Basic Auth vs OAuth Bearer + +| Aspect | Basic Auth | OAuth Bearer Token | +|--------|------------|-------------------| +| **Security** | Lower (credentials in every request) | Higher (token-based, expirable) | +| **Setup** | Simple | Moderate complexity | +| **Token Expiration** | None | Configurable (1-8 hours) | +| **Rotation** | Manual password change | Automatic with refresh tokens | +| **Audit Trail** | Username-based | Token-based (better tracking) | +| **Revocation** | Change password (affects all) | Revoke individual tokens | +| **Best For** | Development/Testing | Production environments | + +--- + +## Troubleshooting + +### Error: "invalid_client" +- Verify Client ID and Client Secret are correct +- Check OAuth application is active in ServiceNow + +### Error: "invalid_grant" +- Check username and password are correct +- Verify user has necessary roles in ServiceNow + +### Error: 401 Unauthorized with Bearer Token +- Token may have expired - refresh it +- Verify token is being sent correctly: `Authorization: Bearer TOKEN` +- Check token wasn't truncated when copying + +--- + +## Next Steps + +1. **Complete OAuth setup** in ServiceNow +2. **Get initial access token** using script or Postman +3. **Store token in Key Vault** (recommended) +4. **Update action configuration** in Simple Chat +5. **Test with agent** to verify authentication works +6. **Set up token refresh** strategy (manual or automated) + +## Related Documentation +- [ServiceNow OAuth Documentation](https://docs.servicenow.com/bundle/xanadu-platform-security/page/administer/security/concept/c_OAuthApplications.html) +- [Simple Chat OpenAPI Basic Auth Fix](./explanation/fixes/OPENAPI_BASIC_AUTH_FIX.md) +- [ServiceNow Integration Guide](./SERVICENOW_INTEGRATION.md) diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml new file mode 100644 index 00000000..4932e7ab --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml @@ -0,0 +1,33 @@ +--- +openapi: "3.0.1" +info: + title: "Knowledge" + description: "Knowledge APIs for Service Portal" + version: "latest" +externalDocs: + url: "" +servers: +- url: "https://dev222288.service-now.com/" +paths: + /api/now/knowledge/search/facets: + get: + description: "" + parameters: [] + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + post: + description: "" + parameters: [] + requestBody: + content: + application/json: {} + responses: + "200": + description: "ok" + content: + application/json: {} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml new file mode 100644 index 00000000..3aaaf7f7 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml @@ -0,0 +1,331 @@ +--- +openapi: "3.0.1" +info: + title: "Table API" + description: "Allows you to perform create, read, update and delete (CRUD) operations\ + \ on existing tables" + version: "latest" +externalDocs: + url: "https://docs.servicenow.com/?context=CSHelp:REST-Table-API" +servers: +- url: "https://dev222288.service-now.com/" +paths: + /api/now/table/{tableName}: + get: + description: "Retrieve records from a table" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sysparm_query" + in: "query" + description: "An encoded query string used to filter the results" + required: false + schema: {} + - name: "sysparm_display_value" + in: "query" + description: "Return field display values (true), actual values (false), or\ + \ both (all) (default: false)" + required: false + schema: {} + - name: "sysparm_exclude_reference_link" + in: "query" + description: "True to exclude Table API links for reference fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_suppress_pagination_header" + in: "query" + description: "True to supress pagination header (default: false)" + required: false + schema: {} + - name: "sysparm_fields" + in: "query" + description: "A comma-separated list of fields to return in the response" + required: false + schema: {} + - name: "sysparm_limit" + in: "query" + description: "The maximum number of results returned per page (default: 10,000)" + required: false + schema: {} + - name: "sysparm_view" + in: "query" + description: "Render the response according to the specified UI view (overridden\ + \ by sysparm_fields)" + required: false + schema: {} + - name: "sysparm_query_category" + in: "query" + description: "Name of the query category (read replica category) to use for\ + \ queries" + required: false + schema: {} + - name: "sysparm_query_no_domain" + in: "query" + description: "True to access data across domains if authorized (default: false)" + required: false + schema: {} + - name: "sysparm_no_count" + in: "query" + description: "Do not execute a select count(*) on table (default: false)" + required: false + schema: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + post: + description: "Create a record" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sysparm_display_value" + in: "query" + description: "Return field display values (true), actual values (false), or\ + \ both (all) (default: false)" + required: false + schema: {} + - name: "sysparm_exclude_reference_link" + in: "query" + description: "True to exclude Table API links for reference fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_fields" + in: "query" + description: "A comma-separated list of fields to return in the response" + required: false + schema: {} + - name: "sysparm_input_display_value" + in: "query" + description: "Set field values using their display value (true) or actual\ + \ value (false) (default: false)" + required: false + schema: {} + - name: "sysparm_suppress_auto_sys_field" + in: "query" + description: "True to suppress auto generation of system fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_view" + in: "query" + description: "Render the response according to the specified UI view (overridden\ + \ by sysparm_fields)" + required: false + schema: {} + requestBody: + content: + application/json: {} + application/xml: {} + text/xml: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + /api/now/table/{tableName}/{sys_id}: + get: + description: "Retrieve a record" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sys_id" + in: "path" + required: true + schema: {} + - name: "sysparm_display_value" + in: "query" + description: "Return field display values (true), actual values (false), or\ + \ both (all) (default: false)" + required: false + schema: {} + - name: "sysparm_exclude_reference_link" + in: "query" + description: "True to exclude Table API links for reference fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_fields" + in: "query" + description: "A comma-separated list of fields to return in the response" + required: false + schema: {} + - name: "sysparm_view" + in: "query" + description: "Render the response according to the specified UI view (overridden\ + \ by sysparm_fields)" + required: false + schema: {} + - name: "sysparm_query_no_domain" + in: "query" + description: "True to access data across domains if authorized (default: false) " + required: false + schema: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + put: + description: "Modify a record" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sys_id" + in: "path" + required: true + schema: {} + - name: "sysparm_display_value" + in: "query" + description: "Return field display values (true), actual values (false), or\ + \ both (all) (default: false)" + required: false + schema: {} + - name: "sysparm_exclude_reference_link" + in: "query" + description: "True to exclude Table API links for reference fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_fields" + in: "query" + description: "A comma-separated list of fields to return in the response" + required: false + schema: {} + - name: "sysparm_input_display_value" + in: "query" + description: "Set field values using their display value (true) or actual\ + \ value (false) (default: false)" + required: false + schema: {} + - name: "sysparm_suppress_auto_sys_field" + in: "query" + description: "True to suppress auto generation of system fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_view" + in: "query" + description: "Render the response according to the specified UI view (overridden\ + \ by sysparm_fields)" + required: false + schema: {} + - name: "sysparm_query_no_domain" + in: "query" + description: "True to access data across domains if authorized (default: false)" + required: false + schema: {} + requestBody: + content: + application/json: {} + application/xml: {} + text/xml: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + delete: + description: "Delete a record" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sys_id" + in: "path" + required: true + schema: {} + - name: "sysparm_query_no_domain" + in: "query" + description: "True to access data across domains if authorized (default: false)" + required: false + schema: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} + patch: + description: "Update a record" + parameters: + - name: "tableName" + in: "path" + required: true + schema: {} + - name: "sys_id" + in: "path" + required: true + schema: {} + - name: "sysparm_display_value" + in: "query" + description: "Return field display values (true), actual values (false), or\ + \ both (all) (default: false)" + required: false + schema: {} + - name: "sysparm_exclude_reference_link" + in: "query" + description: "True to exclude Table API links for reference fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_fields" + in: "query" + description: "A comma-separated list of fields to return in the response" + required: false + schema: {} + - name: "sysparm_input_display_value" + in: "query" + description: "Set field values using their display value (true) or actual\ + \ value (false) (default: false)" + required: false + schema: {} + - name: "sysparm_suppress_auto_sys_field" + in: "query" + description: "True to suppress auto generation of system fields (default:\ + \ false)" + required: false + schema: {} + - name: "sysparm_view" + in: "query" + description: "Render the response according to the specified UI view (overridden\ + \ by sysparm_fields)" + required: false + schema: {} + - name: "sysparm_query_no_domain" + in: "query" + description: "True to access data across domains if authorized (default: false)" + required: false + schema: {} + requestBody: + content: + application/json: {} + application/xml: {} + text/xml: {} + responses: + "200": + description: "ok" + content: + application/json: {} + application/xml: {} + text/xml: {} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml new file mode 100644 index 00000000..7b57cb85 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml @@ -0,0 +1,267 @@ +openapi: 3.0.1 +info: + title: ServiceNow Knowledge Base API + description: ServiceNow Knowledge Management REST API for searching and retrieving knowledge articles - Optimized for Simple Chat integration + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://dev222288.service-now.com + description: ServiceNow Developer Instance + +security: + - bearerAuth: [] + +paths: + /api/now/table/kb_knowledge: + get: + operationId: searchKnowledgeFacets + summary: Search knowledge base articles + description: | + PRIMARY SEARCH FUNCTION: Search ServiceNow knowledge articles. + Use this function to find knowledge articles related to user questions, incidents, or topics. + Search by keywords in article title, content, or category. + Returns published knowledge articles matching the search criteria. + + ⚠️ CRITICAL: PROGRESSIVE SEARCH REQUIRES MULTIPLE FUNCTION CALLS ⚠️ + + You MUST make TWO separate searchKnowledgeFacets calls when first search returns 0 results: + + EXECUTION PATTERN (MAKE THESE AS SEPARATE FUNCTION CALLS): + + CALL 1 - Try Exact Phrase: + 1. Call searchKnowledgeFacets(sysparm_query="textLIKEemail delivery troubleshooting") + 2. Check if result count = 0 + 3. If count > 0: Return results, DONE ✓ + 4. If count = 0: Proceed to CALL 2 (do not give up!) + + CALL 2 - Broad Keyword Fallback: + 1. Extract primary keyword from phrase (e.g., "email" from "email delivery troubleshooting") + 2. Call searchKnowledgeFacets(sysparm_query="textLIKEemail") <-- NEW FUNCTION CALL + 3. Return whatever results are found (likely 5+ articles) + + WHY THIS MATTERS: + - Exact phrase "email delivery troubleshooting" = 0 results (no article has this exact wording) + - Broad keyword "email" = 5+ results (KB0000011, KB0000024, KB0000028, etc.) + - You MUST make the second function call when first returns 0 + + DO NOT: + ❌ Give up after first search returns 0 + ❌ Say "no articles found" without trying broad keyword + ❌ Use complex OR queries in first attempt - keep it simple + + KEYWORD EXTRACTION: + - "email delivery troubleshooting" → primary keyword: "email" + - "spam filter blocking" → primary keyword: "spam" + - "network connectivity issues" → primary keyword: "network" + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: | + Query filter for knowledge articles. Use LIKE operator for text search. + + PROGRESSIVE SEARCH APPROACH: + 1. First attempt: Try exact or close phrase match + - Example: textLIKEemail delivery troubleshooting + 2. If no results: Fall back to primary keyword + - Example: textLIKEemail + 3. If still no results: Try related terms + - Example: textLIKEmail OR textLIKEmessage + + Search patterns: + - Exact phrase: textLIKEemail delivery troubleshooting + - Single keyword: textLIKEemail + - Multiple keywords (OR): textLIKEemail^ORtextLIKEspam + - Title search: short_descriptionLIKEemail + - Category search: kb_categoryLIKEEmail + - Published filter (optional): workflow_state=published^textLIKEemail + + Remember: If a search returns 0 results, try a simpler/broader term + example: "textLIKEemail" + + - name: sysparm_limit + in: query + required: false + schema: + type: integer + default: 10 + maximum: 100 + description: Maximum number of articles to return + example: 10 + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: Comma-separated list of fields to return + example: "number,short_description,text,kb_category,kb_knowledge_base,sys_view_count,workflow_state" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values instead of sys_ids + example: "true" + + responses: + '200': + description: Knowledge articles retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + $ref: '#/components/schemas/KnowledgeArticle' + example: + result: + - number: "KB0000011" + short_description: "How to configure email settings" + text: "Step-by-step guide for configuring email..." + kb_category: "Email" + kb_knowledge_base: "IT Support" + sys_view_count: "145" + workflow_state: "published" + '401': + description: Unauthorized - Invalid credentials + + /api/now/table/kb_knowledge/{sys_id}: + get: + operationId: getKnowledgeArticle + summary: Get specific knowledge article by sys_id + description: Retrieve detailed content of a specific knowledge article + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the knowledge article + example: "abc123xyz789" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "true" + description: Return display values + example: "true" + + responses: + '200': + description: Article retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/KnowledgeArticle' + + '404': + description: Article not found + '401': + description: Unauthorized + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant. + + schemas: + KnowledgeArticle: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Knowledge article number + example: "KB0001234" + + short_description: + type: string + description: Article title/summary + example: "How to configure email server settings" + + text: + type: string + description: Article content/body + example: "To configure email server settings, follow these steps: 1. Navigate to..." + + kb_category: + type: string + description: Knowledge category + example: "Email" + + kb_knowledge_base: + type: string + description: Knowledge base name + example: "IT Support" + + author: + type: string + description: Article author + example: "John Doe" + + workflow_state: + type: string + description: "Publication state: draft, review, published, retired" + example: "published" + + sys_view_count: + type: string + description: Number of times article was viewed + example: "145" + + rating: + type: string + description: Average user rating + example: "4.5" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2025-06-15T10:30:00Z" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-10T14:22:00Z" + + valid_to: + type: string + format: date-time + description: Article expiration date + example: "2027-12-31T23:59:59Z" + + related_links: + type: string + description: Related URLs or references + example: "https://support.example.com/email-guide" + + article_type: + type: string + description: Type of article (how-to, troubleshooting, reference, etc.) + example: "how-to" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml new file mode 100644 index 00000000..e4262b87 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml @@ -0,0 +1,320 @@ +openapi: 3.0.0 +info: + title: ServiceNow Knowledge Base API (Basic Auth) + description: ServiceNow REST API for knowledge article searching and retrieval - Basic Authentication version + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://dev222288.service-now.com + description: ServiceNow Developer Instance + +security: + - basicAuth: [] + +paths: + /api/now/knowledgebase/articles: + get: + operationId: searchKnowledgeFacets + summary: Search knowledge articles with facets + description: | + Search for knowledge articles using text queries and faceted search. + + PROGRESSIVE SEARCH STRATEGY: + The API supports faceted search with text queries. For optimal results: + + 1. INITIAL BROAD SEARCH: + - Use text query only (no facets) to get initial results + - Example: /api/now/knowledgebase/articles?sysparm_query=email + - Response includes article_data AND available facets + + 2. ANALYZE FACETS: + - Review facets returned in response (category, kb_category, workflow_state) + - Identify relevant facets to refine search + + 3. REFINE WITH FACETS (if needed): + - Add specific facet filters to narrow results + - Example: /api/now/knowledgebase/articles?sysparm_query=email&sysparm_facets=kb_category:Network + + 4. RETRIEVE FULL ARTICLE: + - Once you find relevant articles, use getKnowledgeArticle to get full content + - Use the sys_id from search results + + EXAMPLE WORKFLOW: + User: "How do I reset a user's password?" + + Step 1: Initial search + GET /api/now/knowledgebase/articles?sysparm_query=password reset + - Returns 15 articles + - Facets show: IT Support (8), HR (4), Security (3) + + Step 2: Refine with facet (optional if too many results) + GET /api/now/knowledgebase/articles?sysparm_query=password reset&sysparm_facets=kb_category:IT Support + - Returns 8 articles focused on IT Support category + + Step 3: Get full article content + GET /api/now/knowledgebase/articles/{sys_id} + - Returns complete article with all fields + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: | + Text search query. Searches across article text, short description, and keywords. + Example: "email troubleshooting" + example: "email troubleshooting" + + - name: sysparm_facets + in: query + required: false + schema: + type: string + description: | + Facet filters to narrow search results. + Format: facet_field:facet_value + Multiple facets: facet1:value1,facet2:value2 + + Available facets: + - kb_category: Knowledge base category + - category: System category + - workflow_state: Article status (published, draft, retired) + example: "kb_category:IT Support" + + - name: sysparm_limit + in: query + required: false + schema: + type: integer + default: 10 + maximum: 100 + description: Maximum number of articles to return + example: 10 + + - name: sysparm_offset + in: query + required: false + schema: + type: integer + default: 0 + description: Number of articles to skip (for pagination) + example: 0 + + responses: + '200': + description: Search results with articles and facets + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + article_data: + type: array + items: + $ref: '#/components/schemas/KnowledgeArticle' + facets: + type: object + description: Available facets for refining search + additionalProperties: + type: array + items: + type: object + properties: + value: + type: string + count: + type: integer + example: + result: + article_data: + - sys_id: "kb123xyz" + number: "KB0000001" + short_description: "How to reset user password in Active Directory" + text: "Step 1: Open Active Directory Users and Computers..." + kb_category: "IT Support" + workflow_state: "published" + - sys_id: "kb456abc" + number: "KB0000002" + short_description: "Password reset troubleshooting guide" + text: "If password reset fails, check these common issues..." + kb_category: "IT Support" + workflow_state: "published" + facets: + kb_category: + - value: "IT Support" + count: 8 + - value: "HR" + count: 4 + - value: "Security" + count: 3 + workflow_state: + - value: "published" + count: 15 + + '401': + description: Unauthorized - Invalid credentials + '403': + description: Forbidden - Insufficient permissions + + /api/now/knowledgebase/articles/{sys_id}: + get: + operationId: getKnowledgeArticle + summary: Get full knowledge article by sys_id + description: | + Retrieve complete content of a specific knowledge article. + + Use this after searchKnowledgeFacets to get the full article content. + The sys_id comes from the search results. + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the knowledge article + example: "kb123xyz789" + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: | + Comma-separated list of fields to return. + If omitted, returns all fields. + example: "number,short_description,text,kb_category,workflow_state,sys_view_count" + + responses: + '200': + description: Knowledge article retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/KnowledgeArticle' + example: + result: + sys_id: "kb123xyz" + number: "KB0000001" + short_description: "How to reset user password in Active Directory" + text: | +

Password Reset Procedure

+

Follow these steps to reset a user's password:

+
    +
  1. Open Active Directory Users and Computers
  2. +
  3. Navigate to the user's organizational unit
  4. +
  5. Right-click the user account and select 'Reset Password'
  6. +
  7. Enter the new password and confirm
  8. +
  9. Check 'User must change password at next logon' if required
  10. +
  11. Click OK to complete the reset
  12. +
+ kb_category: "IT Support" + category: "Active Directory" + workflow_state: "published" + author: "John Doe" + sys_created_on: "2025-01-15 09:00:00" + sys_updated_on: "2026-01-20 14:30:00" + sys_view_count: "342" + + '404': + description: Article not found + '401': + description: Unauthorized + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: | + HTTP Basic Authentication using ServiceNow username and password. + + Format: username:password (base64 encoded in Authorization header) + Example: Authorization: Basic c2ltcGxlY2hhdDZfaW50ZWdyYXRpb246cGFzc3dvcmQ= + + Note: For production use, consider using Bearer Token authentication instead. + + schemas: + KnowledgeArticle: + type: object + properties: + sys_id: + type: string + description: Unique system identifier for the article + example: "kb123xyz789" + + number: + type: string + description: Knowledge article number + example: "KB0000001" + + short_description: + type: string + description: Brief summary of the article + example: "How to reset user password in Active Directory" + + text: + type: string + description: Full article content (may contain HTML) + example: "

Password Reset Procedure

Follow these steps...

" + + kb_category: + type: string + description: Knowledge base category + example: "IT Support" + + category: + type: string + description: System category + example: "Active Directory" + + workflow_state: + type: string + description: "Article status: published, draft, retired" + example: "published" + + author: + type: string + description: Article author + example: "John Doe" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2025-01-15 09:00:00" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-20 14:30:00" + + sys_view_count: + type: string + description: Number of times article has been viewed + example: "342" + + keywords: + type: string + description: Article keywords for search + example: "password, reset, active directory, user account" + + article_type: + type: string + description: Type of article + example: "How-to" + + valid_to: + type: string + format: date-time + description: Article expiration date + example: "2027-12-31 23:59:59" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml new file mode 100644 index 00000000..f12b8305 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml @@ -0,0 +1,565 @@ +openapi: 3.0.0 +info: + title: ServiceNow Incident Management API + description: ServiceNow REST API for incident management operations - Optimized for Simple Chat integration + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://dev222288.service-now.com/api/now + description: ServiceNow Developer Instance + +security: + - bearerAuth: [] + +paths: + /table/incident: + get: + operationId: queryIncidents + summary: Query incidents with filters + description: Retrieve incidents from ServiceNow based on query parameters and filters + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: | + Encoded query string for filtering results. Examples: + - Last 7 days: sys_created_onONLast 7 days@gs.daysAgoStart(7) + - By priority: priority=1 + - By state: state=1 (1=New, 2=In Progress, 6=Resolved, 7=Closed) + - Combined: priority=1^state=1^sys_created_onONLast 30 days@gs.daysAgoStart(30) + example: "sys_created_onONLast 7 days@gs.daysAgoStart(7)" + + - name: sysparm_limit + in: query + required: false + schema: + type: integer + default: 100 + maximum: 10000 + description: Maximum number of records to return + example: 50 + + - name: sysparm_offset + in: query + required: false + schema: + type: integer + default: 0 + description: Number of records to skip for pagination + example: 0 + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: | + Comma-separated list of fields to return. + CRITICAL: Always include sys_id field - it's required for follow-up queries like getIncidentDetails. + example: "sys_id,number,short_description,priority,state,assigned_to,sys_created_on,category" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values instead of actual values + example: "true" + + responses: + '200': + description: List of incidents retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + $ref: '#/components/schemas/Incident' + example: + result: + - sys_id: "9d385017c611228701d22104cc95c371" + number: "INC0000001" + short_description: "Email server not responding" + priority: "2" + state: "1" + sys_created_on: "2026-01-15 10:30:00" + + '401': + description: Unauthorized - Invalid credentials + '403': + description: Forbidden - Insufficient permissions + + post: + operationId: createIncident + summary: Create a new incident + description: Create a new incident in ServiceNow + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - short_description + properties: + short_description: + type: string + description: Brief summary of the incident + example: "Email server not responding for Finance team" + + description: + type: string + description: Detailed description of the incident + example: "Users in Finance department cannot access email server. Error message: Connection timeout." + + urgency: + type: string + enum: ["1", "2", "3"] + description: "Urgency level: 1=High, 2=Medium, 3=Low" + example: "1" + + priority: + type: string + enum: ["1", "2", "3", "4", "5"] + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + impact: + type: string + enum: ["1", "2", "3"] + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assignment_group: + type: string + description: Assignment group name or sys_id + example: "IT Support" + + assigned_to: + type: string + description: Assigned user name or sys_id + example: "" + + caller_id: + type: string + description: User reporting the incident (name or sys_id) + example: "" + + responses: + '201': + description: Incident created successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + example: + result: + number: "INC0010025" + sys_id: "xyz789abc" + short_description: "Email server not responding for Finance team" + state: "1" + + '400': + description: Bad request - Invalid input + '401': + description: Unauthorized + '403': + description: Forbidden + + /table/incident/{sys_id}: + get: + operationId: getIncidentDetails + summary: Get incident details by sys_id + description: | + Retrieve details of a specific incident. + + CRITICAL: This operation requires the sys_id (NOT the incident number). + - sys_id is a unique identifier like "9d385017c611228701d22104cc95c371" + - Incident number is like "INC0010083" + + When user asks about an incident by number (e.g., "INC0010083"): + 1. First use queryIncidents with sysparm_query=numberLIKEINC0010083 + 2. Get the sys_id from the result + 3. Then call getIncidentDetails with that sys_id + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident + example: "abc123xyz789" + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: Comma-separated list of fields to return + example: "number,short_description,description,priority,state,assigned_to,sys_created_on,resolved_at,close_notes" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values + + responses: + '200': + description: Incident details retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '404': + description: Incident not found + '401': + description: Unauthorized + + put: + operationId: updateIncident + summary: Update an existing incident + description: | + Update incident fields + + ⚠️ CRITICAL: YOU MUST QUERY FOR sys_id FIRST - DO NOT USE CACHED VALUES ⚠️ + + This operation requires the sys_id (NOT the incident number). + - sys_id is a unique identifier like "32ac0eaec326361067d91a2ed40131a7" + - Incident number is like "INC0010095" + + REQUIRED EXECUTION PATTERN (MAKE THESE AS SEPARATE FUNCTION CALLS): + + When user asks to update incident INC0010095: + + CALL 1 - Query to Get sys_id: + 1. Call queryIncidents(sysparm_query="number=INC0010095", sysparm_fields="sys_id,number") + 2. Extract sys_id from result: e.g., "32ac0eaec326361067d91a2ed40131a7" + 3. Verify you got exactly 1 result + + CALL 2 - Update with Retrieved sys_id: + 1. Call updateIncident(sys_id="32ac0eaec326361067d91a2ed40131a7", work_notes="...") + 2. This will succeed because you have the correct sys_id + + WHY THIS MATTERS: + - Wrong sys_id = HTTP 404 "Record doesn't exist" error + - You cannot remember or cache sys_id from previous queries + - Each incident update MUST start with fresh queryIncidents call + - Example: INC0010095 sys_id = "32ac0eaec326361067d91a2ed40131a7" (must query to get this!) + + DO NOT: + ❌ Use sys_id from memory or previous conversation + ❌ Assume sys_id stays the same across conversations + ❌ Skip the queryIncidents call - always query first + ❌ Use sys_id from a different incident + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident to update (must query by incident number first if only number is known) + example: "abc123xyz789" + + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "6" + + work_notes: + type: string + description: | + Add work note (internal journal entry) + + ⚠️ CRITICAL: work_notes is a JOURNAL FIELD ⚠️ + + IMPORTANT BEHAVIOR: + - work_notes is WRITE-ONLY via ServiceNow API + - When you PATCH with work_notes, the API returns HTTP 200 success + - The work note IS written to incident journal history + - BUT work_notes field will ALWAYS return EMPTY in GET requests + - Journal fields do NOT return values - they only accept input + + AFTER ADDING A WORK NOTE: + - Do NOT expect to see it in work_notes field (will be empty) + - Tell user: "Work note added to incident journal successfully" + - Add: "(Note: Work notes are visible in ServiceNow UI, not in API GET responses)" + - The note WAS added even though field shows empty + + EXAMPLE: + You send: PATCH incident/abc123 with body {"work_notes": "Investigating issue"} + API returns: HTTP 200 success with updated incident ✅ + You query: GET incident/abc123 + Result: work_notes="" ← EMPTY is NORMAL, note was still written! + example: "Investigating email server logs. Found timeout errors in SMTP connector." + + close_notes: + type: string + description: Resolution notes (for closing) + example: "Email server restarted. Service restored." + + priority: + type: string + description: Updated priority + example: "3" + + assigned_to: + type: string + description: Reassign to user (name or sys_id) + example: "" + + responses: + '200': + description: Incident updated successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '400': + description: Bad request + '404': + description: Incident not found + '401': + description: Unauthorized + + /stats/incident: + get: + operationId: getIncidentStats + summary: Get incident statistics + description: Retrieve aggregated statistics for incidents + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: Query filter for statistics + example: "sys_created_onONLast 30 days@gs.daysAgoStart(30)" + + - name: sysparm_count + in: query + required: false + schema: + type: boolean + default: true + description: Include count in results + + - name: sysparm_group_by + in: query + required: false + schema: + type: string + description: Field to group by (e.g., priority, category, assigned_to) + example: "category" + + - name: sysparm_avg_fields + in: query + required: false + schema: + type: string + description: Fields to calculate average + example: "business_duration" + + - name: sysparm_sum_fields + in: query + required: false + schema: + type: string + description: Fields to sum + example: "" + + responses: + '200': + description: Statistics retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + type: object + properties: + groupby_value: + type: string + count: + type: string + avg: + type: object + sum: + type: object + example: + result: + - groupby_value: "Email" + count: "45" + - groupby_value: "Network" + count: "32" + - groupby_value: "Hardware" + count: "18" + + '401': + description: Unauthorized + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant. + + schemas: + Incident: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Incident number + example: "INC0000001" + + short_description: + type: string + description: Brief summary + example: "Email server not responding" + + description: + type: string + description: Detailed description + example: "Users cannot access email. Server shows timeout errors." + + priority: + type: string + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + urgency: + type: string + description: "Urgency: 1=High, 2=Medium, 3=Low" + example: "1" + + impact: + type: string + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "1" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assigned_to: + type: string + description: Assigned user + example: "John Doe" + + assignment_group: + type: string + description: Assignment group + example: "IT Support" + + caller_id: + type: string + description: User who reported the incident + example: "Jane Smith" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2026-01-15 10:30:00" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-15 14:22:00" + + opened_at: + type: string + format: date-time + description: When incident was opened + example: "2026-01-15 10:30:00" + + resolved_at: + type: string + format: date-time + description: Resolution timestamp + example: "2026-01-15 16:45:00" + + closed_at: + type: string + format: date-time + description: Closure timestamp + example: "2026-01-16 09:00:00" + + work_notes: + type: string + description: Work notes (internal) + example: "Investigating email server logs" + + close_notes: + type: string + description: Resolution notes + example: "Issue resolved by restarting email service" + + business_duration: + type: string + description: Business hours duration + example: "2 hours 30 minutes" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml new file mode 100644 index 00000000..dc4e1b3a --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml @@ -0,0 +1,570 @@ +openapi: 3.0.0 +info: + title: ServiceNow Incident Management API (Basic Auth) + description: ServiceNow REST API for incident management operations - Basic Authentication version for Simple Chat integration + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://dev222288.service-now.com/api/now + description: ServiceNow Developer Instance + +security: + - basicAuth: [] + +paths: + /table/incident: + get: + operationId: queryIncidents + summary: Query incidents with filters + description: Retrieve incidents from ServiceNow based on query parameters and filters + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: | + Encoded query string for filtering results. Examples: + - Last 7 days: sys_created_onONLast 7 days@gs.daysAgoStart(7) + - By priority: priority=1 + - By state: state=1 (1=New, 2=In Progress, 6=Resolved, 7=Closed) + - Combined: priority=1^state=1^sys_created_onONLast 30 days@gs.daysAgoStart(30) + example: "sys_created_onONLast 7 days@gs.daysAgoStart(7)" + + - name: sysparm_limit + in: query + required: false + schema: + type: integer + default: 100 + maximum: 10000 + description: Maximum number of records to return + example: 50 + + - name: sysparm_offset + in: query + required: false + schema: + type: integer + default: 0 + description: Number of records to skip for pagination + example: 0 + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: | + Comma-separated list of fields to return. + CRITICAL: Always include sys_id field - it's required for follow-up queries like getIncidentDetails. + example: "sys_id,number,short_description,priority,state,assigned_to,sys_created_on,category" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values instead of actual values + example: "true" + + responses: + '200': + description: List of incidents retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + $ref: '#/components/schemas/Incident' + example: + result: + - sys_id: "9d385017c611228701d22104cc95c371" + number: "INC0000001" + short_description: "Email server not responding" + priority: "2" + state: "1" + sys_created_on: "2026-01-15 10:30:00" + + '401': + description: Unauthorized - Invalid credentials + '403': + description: Forbidden - Insufficient permissions + + post: + operationId: createIncident + summary: Create a new incident + description: Create a new incident in ServiceNow + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - short_description + properties: + short_description: + type: string + description: Brief summary of the incident + example: "Email server not responding for Finance team" + + description: + type: string + description: Detailed description of the incident + example: "Users in Finance department cannot access email server. Error message: Connection timeout." + + urgency: + type: string + enum: ["1", "2", "3"] + description: "Urgency level: 1=High, 2=Medium, 3=Low" + example: "1" + + priority: + type: string + enum: ["1", "2", "3", "4", "5"] + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + impact: + type: string + enum: ["1", "2", "3"] + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assignment_group: + type: string + description: Assignment group name or sys_id + example: "IT Support" + + assigned_to: + type: string + description: Assigned user name or sys_id + example: "" + + caller_id: + type: string + description: User reporting the incident (name or sys_id) + example: "" + + responses: + '201': + description: Incident created successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + example: + result: + number: "INC0010025" + sys_id: "xyz789abc" + short_description: "Email server not responding for Finance team" + state: "1" + + '400': + description: Bad request - Invalid input + '401': + description: Unauthorized + '403': + description: Forbidden + + /table/incident/{sys_id}: + get: + operationId: getIncidentDetails + summary: Get incident details by sys_id + description: | + Retrieve details of a specific incident. + + CRITICAL: This operation requires the sys_id (NOT the incident number). + - sys_id is a unique identifier like "9d385017c611228701d22104cc95c371" + - Incident number is like "INC0010083" + + When user asks about an incident by number (e.g., "INC0010083"): + 1. First use queryIncidents with sysparm_query=numberLIKEINC0010083 + 2. Get the sys_id from the result + 3. Then call getIncidentDetails with that sys_id + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident + example: "abc123xyz789" + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: Comma-separated list of fields to return + example: "number,short_description,description,priority,state,assigned_to,sys_created_on,resolved_at,close_notes" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values + + responses: + '200': + description: Incident details retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '404': + description: Incident not found + '401': + description: Unauthorized + + put: + operationId: updateIncident + summary: Update an existing incident + description: | + Update incident fields + + ⚠️ CRITICAL: YOU MUST QUERY FOR sys_id FIRST - DO NOT USE CACHED VALUES ⚠️ + + This operation requires the sys_id (NOT the incident number). + - sys_id is a unique identifier like "32ac0eaec326361067d91a2ed40131a7" + - Incident number is like "INC0010095" + + REQUIRED EXECUTION PATTERN (MAKE THESE AS SEPARATE FUNCTION CALLS): + + When user asks to update incident INC0010095: + + CALL 1 - Query to Get sys_id: + 1. Call queryIncidents(sysparm_query="number=INC0010095", sysparm_fields="sys_id,number") + 2. Extract sys_id from result: e.g., "32ac0eaec326361067d91a2ed40131a7" + 3. Verify you got exactly 1 result + + CALL 2 - Update with Retrieved sys_id: + 1. Call updateIncident(sys_id="32ac0eaec326361067d91a2ed40131a7", work_notes="...") + 2. This will succeed because you have the correct sys_id + + WHY THIS MATTERS: + - Wrong sys_id = HTTP 404 "Record doesn't exist" error + - You cannot remember or cache sys_id from previous queries + - Each incident update MUST start with fresh queryIncidents call + - Example: INC0010095 sys_id = "32ac0eaec326361067d91a2ed40131a7" (must query to get this!) + + DO NOT: + ❌ Use sys_id from memory or previous conversation + ❌ Assume sys_id stays the same across conversations + ❌ Skip the queryIncidents call - always query first + ❌ Use sys_id from a different incident + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident to update (must query by incident number first if only number is known) + example: "abc123xyz789" + + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "6" + + work_notes: + type: string + description: | + Add work note (internal journal entry) + + ⚠️ CRITICAL: work_notes is a JOURNAL FIELD ⚠️ + + IMPORTANT BEHAVIOR: + - work_notes is WRITE-ONLY via ServiceNow API + - When you PATCH with work_notes, the API returns HTTP 200 success + - The work note IS written to incident journal history + - BUT work_notes field will ALWAYS return EMPTY in GET requests + - Journal fields do NOT return values - they only accept input + + AFTER ADDING A WORK NOTE: + - Do NOT expect to see it in work_notes field (will be empty) + - Tell user: "Work note added to incident journal successfully" + - Add: "(Note: Work notes are visible in ServiceNow UI, not in API GET responses)" + - The note WAS added even though field shows empty + + EXAMPLE: + You send: PATCH incident/abc123 with body {"work_notes": "Investigating issue"} + API returns: HTTP 200 success with updated incident ✅ + You query: GET incident/abc123 + Result: work_notes="" ← EMPTY is NORMAL, note was still written! + example: "Investigating email server logs. Found timeout errors in SMTP connector." + + close_notes: + type: string + description: Resolution notes (for closing) + example: "Email server restarted. Service restored." + + priority: + type: string + description: Updated priority + example: "3" + + assigned_to: + type: string + description: Reassign to user (name or sys_id) + example: "" + + responses: + '200': + description: Incident updated successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '400': + description: Bad request + '404': + description: Incident not found + '401': + description: Unauthorized + + /stats/incident: + get: + operationId: getIncidentStats + summary: Get incident statistics + description: Retrieve aggregated statistics for incidents + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: Query filter for statistics + example: "sys_created_onONLast 30 days@gs.daysAgoStart(30)" + + - name: sysparm_count + in: query + required: false + schema: + type: boolean + default: true + description: Include count in results + + - name: sysparm_group_by + in: query + required: false + schema: + type: string + description: Field to group by (e.g., priority, category, assigned_to) + example: "category" + + - name: sysparm_avg_fields + in: query + required: false + schema: + type: string + description: Fields to calculate average + example: "business_duration" + + - name: sysparm_sum_fields + in: query + required: false + schema: + type: string + description: Fields to sum + example: "" + + responses: + '200': + description: Statistics retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + type: object + properties: + groupby_value: + type: string + count: + type: string + avg: + type: object + sum: + type: object + example: + result: + - groupby_value: "Email" + count: "45" + - groupby_value: "Network" + count: "32" + - groupby_value: "Hardware" + count: "18" + + '401': + description: Unauthorized + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: | + HTTP Basic Authentication using ServiceNow username and password. + + Format: username:password (base64 encoded in Authorization header) + Example: Authorization: Basic c2ltcGxlY2hhdDZfaW50ZWdyYXRpb246cGFzc3dvcmQ= + + Note: For production use, consider using Bearer Token authentication instead. + + schemas: + Incident: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Incident number + example: "INC0000001" + + short_description: + type: string + description: Brief summary + example: "Email server not responding" + + description: + type: string + description: Detailed description + example: "Users cannot access email. Server shows timeout errors." + + priority: + type: string + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + urgency: + type: string + description: "Urgency: 1=High, 2=Medium, 3=Low" + example: "1" + + impact: + type: string + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "1" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assigned_to: + type: string + description: Assigned user + example: "John Doe" + + assignment_group: + type: string + description: Assignment group + example: "IT Support" + + caller_id: + type: string + description: User who reported the incident + example: "Jane Smith" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2026-01-15 10:30:00" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-15 14:22:00" + + opened_at: + type: string + format: date-time + description: When incident was opened + example: "2026-01-15 10:30:00" + + resolved_at: + type: string + format: date-time + description: Resolution timestamp + example: "2026-01-15 16:45:00" + + closed_at: + type: string + format: date-time + description: Closure timestamp + example: "2026-01-16 09:00:00" + + work_notes: + type: string + description: Work notes (internal) + example: "Investigating email server logs" + + close_notes: + type: string + description: Resolution notes + example: "Issue resolved by restarting email service" + + business_duration: + type: string + description: Business hours duration + example: "2 hours 30 minutes" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml new file mode 100644 index 00000000..d6425364 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml @@ -0,0 +1,498 @@ +openapi: 3.0.0 +info: + title: ServiceNow Incident Management API + description: ServiceNow REST API for incident management operations - Optimized for Simple Chat integration + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://dev222288.service-now.com/api/now + description: ServiceNow Developer Instance + +security: + - basicAuth: [] + +paths: + /table/incident: + get: + operationId: queryIncidents + summary: Query incidents with filters + description: Retrieve incidents from ServiceNow based on query parameters and filters + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: | + Encoded query string for filtering results. Examples: + - Last 7 days: sys_created_onONLast 7 days@gs.daysAgoStart(7) + - By priority: priority=1 + - By state: state=1 (1=New, 2=In Progress, 6=Resolved, 7=Closed) + - Combined: priority=1^state=1^sys_created_onONLast 30 days@gs.daysAgoStart(30) + example: "sys_created_onONLast 7 days@gs.daysAgoStart(7)" + + - name: sysparm_limit + in: query + required: false + schema: + type: integer + default: 100 + maximum: 10000 + description: Maximum number of records to return + example: 50 + + - name: sysparm_offset + in: query + required: false + schema: + type: integer + default: 0 + description: Number of records to skip for pagination + example: 0 + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: Comma-separated list of fields to return + example: "number,short_description,priority,state,assigned_to,sys_created_on,category" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values instead of actual values + example: "true" + + responses: + '200': + description: List of incidents retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + $ref: '#/components/schemas/Incident' + example: + result: + - number: "INC0000001" + short_description: "Email server not responding" + priority: "2" + state: "1" + sys_id: "abc123xyz" + sys_created_on: "2026-01-15 10:30:00" + + '401': + description: Unauthorized - Invalid credentials + '403': + description: Forbidden - Insufficient permissions + + post: + operationId: createIncident + summary: Create a new incident + description: Create a new incident in ServiceNow + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - short_description + properties: + short_description: + type: string + description: Brief summary of the incident + example: "Email server not responding for Finance team" + + description: + type: string + description: Detailed description of the incident + example: "Users in Finance department cannot access email server. Error message: Connection timeout." + + urgency: + type: string + enum: ["1", "2", "3"] + description: "Urgency level: 1=High, 2=Medium, 3=Low" + example: "1" + + priority: + type: string + enum: ["1", "2", "3", "4", "5"] + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + impact: + type: string + enum: ["1", "2", "3"] + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assignment_group: + type: string + description: Assignment group name or sys_id + example: "IT Support" + + assigned_to: + type: string + description: Assigned user name or sys_id + example: "" + + caller_id: + type: string + description: User reporting the incident (name or sys_id) + example: "" + + responses: + '201': + description: Incident created successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + example: + result: + number: "INC0010025" + sys_id: "xyz789abc" + short_description: "Email server not responding for Finance team" + state: "1" + + '400': + description: Bad request - Invalid input + '401': + description: Unauthorized + '403': + description: Forbidden + + /table/incident/{sys_id}: + get: + operationId: getIncidentDetails + summary: Get incident details by sys_id + description: Retrieve details of a specific incident + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident + example: "abc123xyz789" + + - name: sysparm_fields + in: query + required: false + schema: + type: string + description: Comma-separated list of fields to return + example: "number,short_description,description,priority,state,assigned_to,sys_created_on,resolved_at,close_notes" + + - name: sysparm_display_value + in: query + required: false + schema: + type: string + enum: [true, false, all] + default: "false" + description: Return display values + + responses: + '200': + description: Incident details retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '404': + description: Incident not found + '401': + description: Unauthorized + + put: + operationId: updateIncident + summary: Update an existing incident + description: Update incident fields + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the incident to update + example: "abc123xyz789" + + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "6" + + work_notes: + type: string + description: Work notes to add + example: "Issue resolved by restarting email service" + + close_notes: + type: string + description: Resolution notes (for closing) + example: "Email server restarted. Service restored." + + priority: + type: string + description: Updated priority + example: "3" + + assigned_to: + type: string + description: Reassign to user (name or sys_id) + example: "" + + responses: + '200': + description: Incident updated successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/Incident' + + '400': + description: Bad request + '404': + description: Incident not found + '401': + description: Unauthorized + + /stats/incident: + get: + operationId: getIncidentStats + summary: Get incident statistics + description: Retrieve aggregated statistics for incidents + parameters: + - name: sysparm_query + in: query + required: false + schema: + type: string + description: Query filter for statistics + example: "sys_created_onONLast 30 days@gs.daysAgoStart(30)" + + - name: sysparm_count + in: query + required: false + schema: + type: boolean + default: true + description: Include count in results + + - name: sysparm_group_by + in: query + required: false + schema: + type: string + description: Field to group by (e.g., priority, category, assigned_to) + example: "category" + + - name: sysparm_avg_fields + in: query + required: false + schema: + type: string + description: Fields to calculate average + example: "business_duration" + + - name: sysparm_sum_fields + in: query + required: false + schema: + type: string + description: Fields to sum + example: "" + + responses: + '200': + description: Statistics retrieved successfully + content: + application/json: + schema: + type: object + properties: + result: + type: array + items: + type: object + properties: + groupby_value: + type: string + count: + type: string + avg: + type: object + sum: + type: object + example: + result: + - groupby_value: "Email" + count: "45" + - groupby_value: "Network" + count: "32" + - groupby_value: "Hardware" + count: "18" + + '401': + description: Unauthorized + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: Basic authentication using ServiceNow username and password + + schemas: + Incident: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Incident number + example: "INC0000001" + + short_description: + type: string + description: Brief summary + example: "Email server not responding" + + description: + type: string + description: Detailed description + example: "Users cannot access email. Server shows timeout errors." + + priority: + type: string + description: "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning" + example: "2" + + urgency: + type: string + description: "Urgency: 1=High, 2=Medium, 3=Low" + example: "1" + + impact: + type: string + description: "Impact: 1=High, 2=Medium, 3=Low" + example: "2" + + state: + type: string + description: "State: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled" + example: "1" + + category: + type: string + description: Incident category + example: "Email" + + subcategory: + type: string + description: Incident subcategory + example: "Server" + + assigned_to: + type: string + description: Assigned user + example: "John Doe" + + assignment_group: + type: string + description: Assignment group + example: "IT Support" + + caller_id: + type: string + description: User who reported the incident + example: "Jane Smith" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2026-01-15 10:30:00" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-15 14:22:00" + + opened_at: + type: string + format: date-time + description: When incident was opened + example: "2026-01-15 10:30:00" + + resolved_at: + type: string + format: date-time + description: Resolution timestamp + example: "2026-01-15 16:45:00" + + closed_at: + type: string + format: date-time + description: Closure timestamp + example: "2026-01-16 09:00:00" + + work_notes: + type: string + description: Work notes (internal) + example: "Investigating email server logs" + + close_notes: + type: string + description: Resolution notes + example: "Issue resolved by restarting email service" + + business_duration: + type: string + description: Business hours duration + example: "2 hours 30 minutes" diff --git a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt new file mode 100644 index 00000000..fb0aad8e --- /dev/null +++ b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt @@ -0,0 +1,262 @@ +You are a ServiceNow support specialist with direct API access. Execute actions and show results immediately without narration. + +**CRITICAL: DO NOT narrate what you're doing. Just execute and show data.** + +❌ NEVER say: +- "I will query..." +- "Generating report..." +- "Gathering statistics..." +- "Proceeding with..." +- "Let me check..." +- "I'll analyze..." + +✅ INSTEAD: Execute the action silently and return only the results. + +**You have these operations - use them immediately:** + +From "cio6 ServiceNow - Manage Incidents" action: +- queryIncidents - Query/filter incidents with advanced search +- createIncident - Create new incidents with all fields +- getIncidentDetails - Retrieve full incident details by sys_id +- updateIncident - Update incident state, assignments, work notes, etc. +- getIncidentStats - Get aggregated statistics and metrics + +From "cio6 ServiceNow - Search Knowledge Base" action: +- searchKnowledgeFacets - Search KB articles with progressive search +- getKnowledgeArticle - Retrieve complete article content by sys_id + +**Error Handling:** +- If an action fails, report the error clearly without technical jargon +- For KB search failures: Continue with the main task and note KB articles are unavailable +- Do NOT let one failed action block the entire response +- Example: If KB search fails while analyzing incidents, still show the incident analysis +- Example good error message: "Knowledge articles unavailable at this time. Contact your ServiceNow admin if this persists." + +**Execution Pattern:** + +For READ operations (queries, stats, KB search): +1. Execute action immediately (no announcement) +2. Return formatted results +3. Add brief analysis if requested + +For WRITE operations (create, update): +1. Confirm required parameters if missing +2. Execute after confirmation +3. Return success message with details + +**CRITICAL - Field Mapping for Create/Update Operations:** +When creating or updating incidents, **ALWAYS include ALL fields specified by the user** in the API payload: + +**CRITICAL - Category Field:** +- If user says "Category: Email", you MUST pass EXACTLY: `"category": "Email"` in the JSON request body +- If user says "Category: Network", you MUST pass EXACTLY: `"category": "Network"` in the JSON request body +- NEVER omit the category field when user specifies it +- NEVER substitute a different category value +- ServiceNow will default to "Inquiry / Help" if category field is missing or empty + +**Field Mapping Rules:** +- If user says "Priority: 2", MUST pass `priority: "2"` in the request body +- If user says "assigned to Vivien Chen", MUST pass `assigned_to: "Vivien Chen"` in the request body +- DO NOT skip optional fields that user explicitly requested + +**Required ServiceNow API Field Names:** +- Category → `"category"` (string, e.g., "Email", "Network", "Hardware") +- Priority → `"priority"` (string: "1" to "5") +- State → `"state"` (string: "1" to "8") +- Urgency → `"urgency"` (string: "1" to "3") +- Assigned To → `"assigned_to"` (string: user name or sys_id) +- Assignment Group → `"assignment_group"` (string: group name or sys_id) +- Short Description → `"short_description"` (string, required) +- Description → `"description"` (string: detailed notes) + +**Example Create Incident Payload:** +```json +{ + "short_description": "Email server not responding", + "description": "Users cannot access Outlook", + "category": "Email", + "priority": "2", + "urgency": "1", + "assigned_to": "Vivien Chen" +} +``` + +**Response Format Examples:** + +Query request: "Show open critical incidents" +❌ Bad: "I'll query critical incidents with state=1 and priority=1..." +✅ Good: [Execute query] "Here are 5 open critical incidents: + +| Number | Priority | Description | Created | +|--------|----------|-------------|---------| +| INC001 | Critical | Email down | Jan 22 | +..." + +Statistics request: "How many incidents last month?" +❌ Bad: "Let me gather statistics for December 2025..." +✅ Good: [Execute stats] "December 2025 Incident Statistics: + +- Total incidents: 127 +- Critical: 15 (12%) +- High: 42 (33%) +- Medium: 70 (55%) + +Top categories: +1. Email (45 incidents) +2. Network (32 incidents) +3. Hardware (28 incidents)" + +Monthly report request: "Create monthly support report" +❌ Bad: "Generating monthly report... Gathering statistics... Proceeding with analysis..." +✅ Good: [Execute query for last month] "December 2025 Support Report: + +**Incident Volume:** +- Total: 127 incidents +- Critical: 15 | High: 42 | Medium: 70 + +**Top Issues:** +| Category | Count | % of Total | +|----------|-------|------------| +| Email | 45 | 35% | +| Network | 32 | 25% | +| Hardware | 28 | 22% | + +**Resolution Metrics:** +- Avg resolution time: 4.2 hours +- Same-day resolution: 78% + +**Recommendations:** +- Email issues increasing 15% vs November +- Consider additional email server monitoring +- Network incidents clustering Tue/Thu - investigate" + +**Formatting Rules:** +- Use markdown tables for lists of incidents +- Include: incident number, priority, state, description, dates +- Show metrics as bullet points or small tables +- Add brief recommendations when doing analysis + +**CRITICAL - Display ALL Records User Requests:** +- When user says "show 100 records", display ALL 100 rows in the table (not just a preview) +- When user says "top 200", display ALL 200 rows in the table +- DO NOT truncate tables to "first 10" or add "..." for more +- If user doesn't specify a number, default to showing 10 records only +- For very large requests (500+), you may summarize instead of full table - ask user first + +**CRITICAL - API Parameter Requirements:** +- **ALWAYS include sysparm_limit parameter** in queryIncidents action +- ServiceNow API default is 100 results if sysparm_limit is not specified +- When user says "return 100 records", you MUST pass sysparm_limit=100 +- When user says "top 200", you MUST pass sysparm_limit=200 +- When user says "top 10", you MUST pass sysparm_limit=10 +- When user doesn't specify, use sysparm_limit=10 (NOT the API default of 100) +- Maximum allowed: sysparm_limit=10000 + +**CRITICAL - Incident Updates with sys_id:** +⚠️ YOU MUST QUERY FOR sys_id FIRST - DO NOT USE CACHED VALUES ⚠️ + +When user asks to update incident INC0010095: + +CALL 1 - Query to Get sys_id: +1. Call queryIncidents(sysparm_query="number=INC0010095", sysparm_fields="sys_id,number") +2. Extract sys_id from result: e.g., "32ac0eaec326361067d91a2ed40131a7" +3. Verify you got exactly 1 result + +CALL 2 - Update with Retrieved sys_id: +1. Call updateIncident(sys_id="32ac0eaec326361067d91a2ed40131a7", work_notes="...") +2. This will succeed because you have the correct sys_id + +WHY THIS MATTERS: +- Wrong sys_id = HTTP 404 "Record doesn't exist" error +- You cannot remember or cache sys_id from previous queries +- Each incident update MUST start with fresh queryIncidents call + +DO NOT: +❌ Use sys_id from memory or previous conversation +❌ Assume sys_id stays the same across conversations +❌ Skip the queryIncidents call - always query first +❌ Use sys_id from a different incident + +**Date/Time Understanding:** + +CRITICAL: Understand the difference between incident STATE and incident DATE: + +❌ WRONG Interpretation: +- "Show open incidents today" → Query for state=New AND created_on=today + - This is WRONG because "open" refers to STATE, not creation date + +✅ CORRECT Interpretation: +- "Show open incidents today" → Query for state=1 (New) OR state=2 (In Progress) + - Shows all incidents currently in open states, regardless of creation date + +- "Show incidents created today" → Query for sys_created_on=today + - Shows all incidents created today, regardless of current state + +**Common date/time queries:** +- "open incidents" = state=1 or state=2 (New or In Progress) +- "closed incidents" = state=7 (Closed) or state=6 (Resolved) +- "incidents created today" = sys_created_on >= start of today +- "incidents created last 7 days" = sys_created_on >= 7 days ago +- "resolved incidents today" = state=6 AND resolved_at >= start of today +- "open incidents created yesterday" = (state=1 OR state=2) AND sys_created_on between yesterday start/end + +**State values:** +- 1 = New +- 2 = In Progress +- 3 = On Hold +- 6 = Resolved +- 7 = Closed +- 8 = Canceled + +**Query examples:** +``` +"Show me top 200 open incidents" +→ Execute with: state IN (1,2), sysparm_limit=200 +→ Return all 200 in table, no commentary about "more available" + +"Show open incidents created today" +→ Execute with: (state=1 OR state=2) AND sys_created_on >= today +→ This shows NEW/IN PROGRESS incidents that were CREATED today + +"Show incidents resolved today" +→ Execute with: state=6 AND resolved_at >= today +→ This shows incidents that reached RESOLVED state today +``` + +**CRITICAL - Knowledge Base Progressive Search:** +⚠️ PROGRESSIVE SEARCH REQUIRES MULTIPLE FUNCTION CALLS ⚠️ + +You MUST make TWO separate searchKnowledgeFacets calls when first search returns 0 results: + +CALL 1 - Try Exact Phrase: +1. Call searchKnowledgeFacets(sysparm_query="textLIKEemail delivery troubleshooting") +2. Check if result count = 0 +3. If count > 0: Return results, DONE ✓ +4. If count = 0: Proceed to CALL 2 (do not give up!) + +CALL 2 - Broad Keyword Fallback: +1. Extract primary keyword from phrase (e.g., "email" from "email delivery troubleshooting") +2. Call searchKnowledgeFacets(sysparm_query="textLIKEemail") <-- NEW FUNCTION CALL +3. Return whatever results are found (likely 5+ articles) + +WHY THIS MATTERS: +- Exact phrase "email delivery troubleshooting" = 0 results (no article has this exact wording) +- Broad keyword "email" = 5+ results (KB0000011, KB0000024, KB0000028, etc.) +- You MUST make the second function call when first returns 0 + +DO NOT: +❌ Give up after first search returns 0 +❌ Say "no articles found" without trying broad keyword +❌ Use complex OR queries in first attempt - keep it simple + +KEYWORD EXTRACTION: +- "email delivery troubleshooting" → primary keyword: "email" +- "spam filter blocking" → primary keyword: "spam" +- "network connectivity issues" → primary keyword: "network" + +**Work Notes Timing:** +- Work notes updates may take a few moments to appear in subsequent queries +- Do not be alarmed if work_notes field appears empty immediately after update +- The update was successful - ServiceNow processes journal entries asynchronously + +**Key Principle: Results first, no process narration. Honor user's limits exactly.** From 502355f394a6e671da6807010b5d08313915a683 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 19:17:20 -0500 Subject: [PATCH 03/35] Removed the readme files for bug fix details --- .../AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 226 ---------- .../GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 403 ------------------ docs/fixes/GROUP_AGENT_LOADING_FIX.md | 241 ----------- docs/fixes/OPENAPI_BASIC_AUTH_FIX.md | 205 --------- 4 files changed, 1075 deletions(-) delete mode 100644 docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md delete mode 100644 docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md delete mode 100644 docs/fixes/GROUP_AGENT_LOADING_FIX.md delete mode 100644 docs/fixes/OPENAPI_BASIC_AUTH_FIX.md diff --git a/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md deleted file mode 100644 index 0d507ecc..00000000 --- a/docs/fixes/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md +++ /dev/null @@ -1,226 +0,0 @@ -# Azure AI Search Test Connection Fix - -## Issue Description - -When clicking the "Test Azure AI Search Connection" button on the App Settings "Search & Extract" page with **managed identity authentication** enabled, the test connection failed with the following error: - -**Original Error Message:** -``` -NameError: name 'search_resource_manager' is not defined -``` - -**Environment Configuration:** -- Authentication Type: Managed Identity -- Azure Environment: `public` (set in .env file) -- Error occurred because the code tried to reference `search_resource_manager` which wasn't defined for public cloud - -**Root Cause:** The old implementation used a REST API approach that required the `search_resource_manager` variable to construct authentication tokens. This variable wasn't defined for the public cloud environment, causing the initial error. Even if defined, the REST API approach with bearer tokens doesn't work properly with Azure AI Search's managed identity authentication. - -## Root Cause Analysis - -The old implementation used a **REST API approach with manually acquired bearer tokens**, which is fundamentally incompatible with how Azure AI Search handles managed identity authentication on the data plane. - -### Why the Old Approach Failed - -Azure AI Search's data plane operations don't properly accept bearer tokens acquired through standard `DefaultAzureCredential.get_token()` flows and passed as HTTP Authorization headers. The authentication mechanism works differently: - -```python -# OLD IMPLEMENTATION - FAILED ❌ -credential = DefaultAzureCredential() -arm_scope = f"{search_resource_manager}/.default" -token = credential.get_token(arm_scope).token - -headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json" -} -response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) -# Returns: 403 Forbidden -``` - -**Problems with this approach:** -1. Azure AI Search requires SDK-specific authentication handling -2. Bearer tokens from `get_token()` are rejected by the Search service -3. Token scope and refresh logic need specialized handling -4. This issue occurs in **all Azure environments** (public, government, custom) - -### Why Other Services Work with REST API + Bearer Tokens - -Some Azure services accept bearer tokens in REST API calls, but Azure AI Search requires the SDK to: -1. Acquire tokens using the correct scope and flow -2. Handle token refresh automatically -3. Use Search-specific authentication headers -4. Properly negotiate with the Search service's auth layer - -## Technical Details - -### Files Modified - -**File:** `route_backend_settings.py` -**Function:** `_test_azure_ai_search_connection(payload)` -**Lines:** 760-796 - -### The Solution - -Instead of trying to define `search_resource_manager` for public cloud, the fix was to **replace the REST API approach entirely with the SearchIndexClient SDK**, which handles authentication correctly without needing the `search_resource_manager` variable. - -### Code Changes Summary - -**Before (REST API approach):** -```python -def _test_azure_ai_search_connection(payload): - # ... setup code ... - - if direct_data.get('auth_type') == 'managed_identity': - credential = DefaultAzureCredential() - arm_scope = f"{search_resource_manager}/.default" - token = credential.get_token(arm_scope).token - - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json" - } - response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) - # ❌ Returns 403 Forbidden -``` - -**After (SDK approach):** -```python -def _test_azure_ai_search_connection(payload): - # ... setup code ... - - if direct_data.get('auth_type') == 'managed_identity': - credential = DefaultAzureCredential() - - # Use SDK which handles authentication properly - if AZURE_ENVIRONMENT in ("usgovernment", "custom"): - client = SearchIndexClient( - endpoint=endpoint, - credential=credential, - audience=search_resource_manager - ) - else: - # For public cloud, don't use audience parameter - client = SearchIndexClient( - endpoint=endpoint, - credential=credential - ) - - # Test by listing indexes (simple operation to verify connectivity) - indexes = list(client.list_indexes()) - # ✅ Works correctly -``` - -### Key Implementation Details - -1. **Replaced REST API with SearchIndexClient SDK** - - Uses `SearchIndexClient` from `azure.search.documents` - - SDK handles authentication internally - - Properly manages token acquisition and refresh - -2. **Environment-Specific Configuration** - - **Azure Government/Custom:** Requires `audience` parameter - - **Azure Public Cloud:** Omits `audience` parameter - - Matches pattern used throughout codebase - -3. **Consistent with Other Functions** - - Aligns with `get_index_client()` implementation (line 484) - - Matches SearchClient initialization in `config.py` (lines 584-619) - - All other search operations already use SDK approach - -## Testing Approach - -### Prerequisites -- Service principal must have **"Search Index Data Contributor"** RBAC role -- Permissions must propagate (5-10 minutes after assignment) - -### RBAC Role Assignment Command -```bash -az role assignment create \ - --assignee \ - --role "Search Index Data Contributor" \ - --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ -``` - -### Verification -```bash -az role assignment list \ - --assignee \ - --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ \ - --output table -``` - -## Impact Analysis - -### What Changed -- **Only the test connection function** was affected -- No changes needed to actual search operations (indexing, querying, etc.) -- All other search functionality already used correct SDK approach - -### Why Other Search Operations Weren't Affected -All production search operations throughout the codebase already use the SDK: -- `SearchClient` for querying indexes -- `SearchIndexClient` for managing indexes -- `get_index_client()` helper function -- Index initialization in `config.py` - -**Only the test connection function used the failed REST API approach.** - -## Validation - -### Before Fix -- ✅ Authentication succeeded (no credential errors) -- ✅ Token acquisition worked -- ❌ Azure AI Search rejected bearer token (403 Forbidden) -- ❌ Test connection failed - -### After Fix -- ✅ Authentication succeeds -- ✅ SDK handles token acquisition properly -- ✅ Azure AI Search accepts SDK authentication -- ✅ Test connection succeeds (with proper RBAC permissions) - -## Configuration Requirements - -### Public Cloud (.env) -```ini -AZURE_ENVIRONMENT=public -AZURE_CLIENT_ID= -AZURE_CLIENT_SECRET= -AZURE_TENANT_ID= -AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity -AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.net -``` - -### Azure Government (.env) -```ini -AZURE_ENVIRONMENT=usgovernment -AZURE_CLIENT_ID= -AZURE_CLIENT_SECRET= -AZURE_TENANT_ID= -AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity -AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.us -``` - -## Related Changes - -**No config.py changes were made.** The fix was entirely in the route_backend_settings.py file by switching from REST API to SDK approach. - -The SDK approach eliminates the need for the `search_resource_manager` variable in public cloud because: -- The SearchIndexClient handles authentication internally -- No manual token acquisition is needed -- The SDK knows the correct endpoints and scopes automatically - -## Version Information - -**Version Implemented:** 0.235.004 - -## References - -- Azure AI Search SDK Documentation: https://learn.microsoft.com/python/api/azure-search-documents -- Azure RBAC for Search: https://learn.microsoft.com/azure/search/search-security-rbac -- DefaultAzureCredential: https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential - -## Summary - -The fix replaces manual REST API calls with the proper Azure Search SDK (`SearchIndexClient`), which correctly handles managed identity authentication for Azure AI Search. This aligns the test function with all other search operations in the codebase that already use the SDK approach successfully. diff --git a/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md deleted file mode 100644 index 196bc132..00000000 --- a/docs/fixes/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md +++ /dev/null @@ -1,403 +0,0 @@ -# Group Action OAuth Authentication and Schema Merging Fix - -## Header Information - -**Fix Title:** Group Actions Missing `additionalFields` Causing OAuth Authentication Failures -**Issue Description:** Group actions were missing the `additionalFields` property entirely, preventing OAuth bearer token authentication from working despite having the same configuration as working global actions. -**Root Cause:** Group action backend routes did not call `get_merged_plugin_settings()` to merge UI form data with schema defaults, while global action routes did. This caused group actions to be saved without authentication configuration fields. -**Version Implemented:** 0.235.028 -**Date:** January 22, 2026 - -## Problem Statement - -### Symptoms -When a group action was configured with OAuth bearer token authentication: -- Action execution returned **HTTP 401 Unauthorized** errors -- ServiceNow API responded: `{"error":{"message":"User is not authenticated"}}` -- UI displayed `additionalFields: {}` (empty object) when editing group action -- Global action with identical configuration showed populated `additionalFields` and worked correctly -- Bearer token header was not being sent in API requests - -### Impact -- **Severity:** High - OAuth authentication completely non-functional for group actions -- **Affected Users:** All users attempting to use group actions with OAuth/Bearer token authentication -- **Workaround:** Use global actions instead of group actions (not scalable) - -### Evidence from Logs -``` -[DEBUG] Auth type: bearer -[DEBUG] Token available: True -[DEBUG] Added bearer auth: EfP7otqXmV... -[DEBUG] Making request to https://dev222288.service-now.com/api/now/table/incident -[DEBUG] Request headers: {'Authorization': 'Bearer EfP7otqXmV...', ...} -[DEBUG] Response status: 401 -[DEBUG] Response text: {"error":{"message":"User is not authenticated",...}} -``` - -**Critical Discovery:** When comparing global vs group action data: -- **Global action** (working): `additionalFields: {auth_method: 'bearer', base_url: '...', ...}` -- **Group action** (failing): `additionalFields: {}` ← Empty object! - -## Root Cause Analysis - -### Backend Route Disparity - -#### Global Action Routes (Working) -**File:** `route_backend_plugins.py` - Lines 666-667 (add_plugin route) - -```python -# Global action creation route -merged = get_merged_plugin_settings( - plugin_type, - current_settings=additionalFields, - schema_dir=schema_dir -) -``` - -**Result:** UI form data is merged with schema defaults, preserving authentication configuration sent from JavaScript. - -#### Group Action Routes (Broken - Before Fix) -**File:** `route_backend_plugins.py` - Lines 430-470, 485-530 - -```python -# Group action creation/update routes - BEFORE FIX -# NO CALL to get_merged_plugin_settings() -# additionalFields saved directly from request without merging -``` - -**Result:** `additionalFields` data from UI was not being preserved, resulting in empty objects. - -### Data Flow Architecture - -The fix revealed the actual data flow for authentication configuration: - -1. **UI Layer** (`plugin_modal_stepper.js` line 1537): - ```javascript - additionalFields.auth_method = 'bearer'; // Set by JavaScript based on dropdown - ``` - -2. **HTTP POST** to backend: - ```json - { - "name": "action_name", - "auth": {"type": "key"}, - "additionalFields": { - "auth_method": "bearer", - "base_url": "https://dev222288.service-now.com/api/now" - } - } - ``` - -3. **Backend Processing** - `get_merged_plugin_settings()`: - - **If schema file exists:** Merge UI data with schema defaults - - **If schema file missing:** Return UI data unchanged (graceful fallback) - - **If function not called:** Data lost! - -4. **Storage:** Cosmos DB saves merged data - -### Why Global Actions Worked Without Schema File - -**Key Insight:** The `openapi_plugin.additional_settings.schema.json` file **never existed** for global actions either! - -Global actions worked because: -1. Backend routes **called** `get_merged_plugin_settings()` -2. Function detected missing schema file -3. **Graceful fallback** (lines 110-114 in `functions_plugins.py`): - ```python - else: - result[nested_key] = current_val # Return UI data unchanged - ``` -4. UI data passed through and was saved correctly - -Group actions failed because: -1. Backend routes **did not call** the merge function at all -2. `additionalFields` from UI was discarded -3. Empty object `{}` saved to database -4. OAuth configuration lost - -## Technical Details - -### Files Modified - -1. **`route_backend_plugins.py`** (Lines 430-530) - - **Line 461-463** (create_group_action_route): Added schema merging - - **Line 520-522** (update_group_action_route): Added schema merging - - **Parity achieved:** Both global and group routes now call `get_merged_plugin_settings()` - -2. **`config.py`** - - Updated VERSION from "0.235.027" to "0.235.028" - -### Code Changes - -#### Group Action Creation Route - BEFORE -```python -def create_group_action_route(user_id, group_id): - """Create new group action""" - data = request.get_json() - # ... validation ... - - # Direct save without merging - saved_plugin = save_group_action( - user_id=user_id, - group_id=group_id, - plugin_data=data # additionalFields lost here! - ) -``` - -#### Group Action Creation Route - AFTER (Fixed) -```python -def create_group_action_route(user_id, group_id): - """Create new group action""" - data = request.get_json() - # ... validation ... - - # NEW: Merge additionalFields with schema defaults (lines 461-463) - merged = get_merged_plugin_settings( - plugin_type=data.get('type', 'openapi'), - current_settings=data.get('additionalFields', {}), - schema_dir=schema_dir - ) - data['additionalFields'] = merged - - saved_plugin = save_group_action( - user_id=user_id, - group_id=group_id, - plugin_data=data # Now includes preserved auth config! - ) -``` - -**Same fix applied to:** -- `update_group_action_route()` (lines 520-522) - -### Graceful Fallback Behavior - -**File:** `functions_plugins.py` (Lines 92-115) - -```python -def get_merged_plugin_settings(plugin_type, current_settings, schema_dir): - """ - Merge plugin settings with schema defaults. - - If schema file doesn't exist: returns current_settings unchanged. - This is intentional - allows UI-driven configuration. - """ - schema_path = os.path.join(schema_dir, f"{plugin_type}.additional_settings.schema.json") - - if not os.path.exists(schema_path): - # Graceful fallback - return UI data as-is (lines 110-114) - result = {} - for nested_key in current_settings: - result[nested_key] = current_settings[nested_key] # Preserve UI data - return result - - # If schema exists, merge with defaults - # ... -``` - -**Design Decision:** Schema files are **optional** - the system works perfectly with UI-driven configuration via graceful fallback. - -## Solution Implemented - -### Fix Strategy -1. ✅ Add `get_merged_plugin_settings()` calls to group action routes (parity with global routes) -2. ✅ Rely on UI-driven configuration + backend graceful fallback (proven approach) -3. ✅ Require recreation of existing group actions to populate `additionalFields` - -### Architecture Result - -**Both global and group routes now have identical behavior:** - -1. **UI sends complete `additionalFields`** from form -2. **Backend calls `get_merged_plugin_settings()`** for parity -3. **Function detects no schema file** exists -4. **Graceful fallback returns UI data unchanged** -5. **Complete authentication config saved** to database - -**Benefits:** -- ✅ Simple: UI drives configuration, backend preserves it -- ✅ Proven: Global actions validate this approach -- ✅ Maintainable: No schema files to keep in sync -- ✅ Flexible: Easy to extend authentication types in UI - -## Validation - -### Test Procedure -1. Delete existing group action (has empty `additionalFields`) -2. Create new group action via UI: - - Type: OpenAPI - - Upload ServiceNow spec - - Base URL: `https://dev222288.service-now.com/api/now` - - Authentication: **Bearer Token** (dropdown selection) - - Token: `EfP7otqXmVmg06xfB9igagxL6Pjir7ewv99sZyMqYdzImlerPt9rHM1T1_L8cCEeWZAuWUV0GPDP2eZ56XWoEQ` -3. UI JavaScript sets `additionalFields.auth_method = 'bearer'` (line 1537) -4. Backend merge function preserves UI data via fallback -5. Action saved with complete authentication configuration - -### Expected Results -- ✅ Group action `additionalFields` populated: `{auth_method: 'bearer', base_url: '...', ...}` -- ✅ ServiceNow API calls return **HTTP 200** instead of 401 -- ✅ Authorization header sent: `Bearer EfP7otqXmV...` -- ✅ Group agent successfully queries ServiceNow incidents -- ✅ Edit group action page displays authentication fields correctly - -## Impact Analysis - -### Before Fix -- **Global actions:** ✅ Working - routes call merge function -- **Group actions:** ❌ Broken - routes don't call merge function -- **Result:** OAuth authentication impossible for group actions - -### After Fix -- **Global actions:** ✅ Working - routes call merge function → fallback preserves UI data -- **Group actions:** ✅ Working - routes call merge function → fallback preserves UI data -- **Result:** Complete parity, OAuth authentication works for both - -### Breaking Changes -**None** - This is a pure fix with backward compatibility: -- Existing global actions continue working (unchanged code path) -- **New/recreated** group actions now work correctly -- Existing broken group actions remain broken until recreated (user action required) - -## Lessons Learned - -### Key Insights -1. **UI is source of truth for authentication config** - Backend preserves what UI sends -2. **Graceful fallback is a feature, not a bug** - Enables UI-driven configuration -3. **Code parity prevents subtle bugs** - Global and group routes should be identical -4. **Testing existing functionality reveals architecture** - Global actions proved UI approach works - -### Best Practices Reinforced -- **Investigate working code before making changes** - Global actions showed the pattern -- **Prefer simplicity** - UI-driven configuration simpler than complex schema systems -- **Document data flows** - Understanding UI → Backend → DB flow was crucial -- **Test parity** - If code paths differ, investigate why - -## Related Documentation -- **[Group Agent Loading Fix](./GROUP_AGENT_LOADING_FIX.md)** - Prerequisites for this fix (v0.235.027) -- **ServiceNow OAuth Setup** - Configuration instructions for OAuth 2.0 bearer tokens -- **Plugin Modal Stepper** - UI component responsible for authentication form (`plugin_modal_stepper.js`) - -## Future Considerations - -### ⚠️ CRITICAL: OAuth 2.0 Token Expiration Limitation - -**Current Implementation Status:** -- ✅ **Bearer token authentication works correctly** - tokens are sent properly in HTTP headers -- ❌ **No automatic token refresh** - requires manual regeneration when expired -- ⚠️ **Production limitation** - not suitable for production use without enhancement - -**The Problem:** -ServiceNow OAuth access tokens expire after a configured lifespan (e.g., 3,600 seconds = 1 hour). The current Simple Chat implementation: - -1. **Stores static bearer tokens** - copied from ServiceNow and hardcoded in action configuration -2. **No expiration tracking** - doesn't know when token will expire -3. **No refresh mechanism** - can't automatically request new tokens -4. **Manual workaround required** - users must regenerate and update token every hour - -**Example Failure:** -``` -Request: GET https://dev222288.service-now.com/api/now/table/incident -Headers: Authorization: Bearer EfP7otqXmV... (expired token) -Response: HTTP 401 - {"error":{"message":"User is not authenticated"}} -``` - -**Temporary Testing Workaround:** -- Increase ServiceNow "Access Token Lifespan" to longer duration (e.g., 86,400 seconds = 24 hours) -- Regenerate token before expiration -- **Not suitable for production environments** - -**Proper Solution Required (Future Enhancement):** - -To make OAuth 2.0 authentication production-ready, Simple Chat needs to implement the OAuth 2.0 Client Credentials flow with automatic token refresh: - -#### Required Components: - -1. **Store OAuth Client Credentials** (Not Bearer Token): - ```json - { - "auth_type": "oauth2_client_credentials", - "client_id": "565d53a80dfe4cb89b8869fd1d977308", - "client_secret": "[encrypted_secret]", - "token_endpoint": "https://dev222288.service-now.com/oauth_token.do", - "scope": "useraccount" - } - ``` - -2. **Token Storage with Expiration Tracking**: - ```python - { - "access_token": "EfP7otqXmV...", - "refresh_token": "abc123...", - "expires_at": "2026-01-22T20:17:39Z", # Timestamp - "token_type": "bearer" - } - ``` - -3. **Automatic Token Refresh Logic**: - ```python - def get_valid_token(action_config): - """Get valid token, refreshing if expired""" - if token_expired(action_config): - # Call ServiceNow OAuth token endpoint - response = requests.post( - action_config['token_endpoint'], - data={ - 'grant_type': 'client_credentials', - 'client_id': action_config['client_id'], - 'client_secret': decrypt(action_config['client_secret']) - } - ) - # Update stored token with new access_token and expires_at - update_token_storage(response.json()) - - return get_current_token() - ``` - -4. **Pre-Request Token Validation**: - ```python - # Before each API call in openapi_plugin.py - if auth_config['type'] == 'oauth2_client_credentials': - auth_config['token'] = get_valid_token(auth_config) - headers['Authorization'] = f"Bearer {auth_config['token']}" - ``` - -5. **Secure Secret Storage**: - - Store client secrets in Azure Key Vault (not in Cosmos DB) - - Use Managed Identity for Key Vault access - - Encrypt secrets at rest - -#### Implementation Tasks: - -- [ ] **UI Changes**: Add OAuth 2.0 configuration form (Client ID, Secret, Token Endpoint) -- [ ] **Backend Changes**: - - [ ] Create `oauth2_token_manager.py` module for token lifecycle management - - [ ] Implement token refresh logic with expiration checking - - [ ] Add Key Vault integration for client secret storage - - [ ] Update `openapi_plugin_factory.py` to detect OAuth 2.0 auth type - - [ ] Modify HTTP request preparation to request fresh tokens -- [ ] **Database Schema**: Add token storage fields (access_token, refresh_token, expires_at) -- [ ] **Testing**: End-to-end testing with real OAuth 2.0 endpoints and token expiration scenarios -- [ ] **Documentation**: Update user guide with OAuth 2.0 setup instructions - -#### References: -- [OAuth 2.0 Client Credentials Grant](https://oauth.net/2/grant-types/client-credentials/) -- [ServiceNow OAuth 2.0 Documentation](https://docs.servicenow.com/bundle/washingtondc-platform-security/page/administer/security/concept/c_OAuthApplications.html) -- [Azure Key Vault for Secret Management](https://learn.microsoft.com/azure/key-vault/general/overview) - -**Estimated Effort:** 2-3 weeks for complete implementation and testing - -**Priority:** Medium - Current manual workaround functional for testing/development, critical for production deployment - ---- - -### Monitoring -Track authentication failures by action type to detect similar issues: -```python -# Example monitoring -if response.status_code == 401: - logger.warning(f"Auth failed for {action_type} action: {action_name}") -``` - -## Version History -- **0.235.027** - Group agent loading fix (prerequisite) -- **0.235.028** - Group action schema merging parity fix (this document) diff --git a/docs/fixes/GROUP_AGENT_LOADING_FIX.md b/docs/fixes/GROUP_AGENT_LOADING_FIX.md deleted file mode 100644 index 62389eb9..00000000 --- a/docs/fixes/GROUP_AGENT_LOADING_FIX.md +++ /dev/null @@ -1,241 +0,0 @@ -# Group Agent Loading Fix - -## Header Information - -**Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode -**Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. -**Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. -**Version Implemented:** 0.235.027 -**Date:** January 22, 2026 - -## Problem Statement - -### Symptoms -When a user selected a group agent in per-user semantic kernel mode: -- The agent selection would fall back to the global "researcher" agent -- Plugin count would be zero (`plugin_count: 0, plugins: []`) -- Agent would ask clarifying questions instead of executing available actions -- No group agents appeared in the available agents list -- Group actions (plugins) were not accessible even though they existed in the database - -### Impact -- **Severity:** High - Group agents completely non-functional in per-user kernel mode -- **Affected Users:** All users with per-user semantic kernel enabled who are members of groups -- **Workaround:** None - only global agents worked - -### Evidence from Logs -``` -[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' -[SK Loader] Found 2 global agents to merge -[SK Loader] After merging: 3 total agents -[DEBUG] [INFO]: [SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False)] -[DEBUG] [INFO]: [SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False -[DEBUG] [INFO]: [SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent -[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c No agent found matching user-selected agent: cio6_servicenow_test_agent -``` - -Notice: Only 3 agents loaded (2 global + 1 personal), **zero group agents** despite user being member of group "cio6". - -## Root Cause Analysis - -### Architectural Gap -The `load_user_semantic_kernel()` function in `semantic_kernel_loader.py` had the following loading sequence: - -1. ✅ Load personal agents via `get_personal_agents(user_id)` -2. ✅ Conditionally merge global agents if `merge_global_semantic_kernel_with_workspace` enabled -3. ❌ **MISSING:** Load group agents from user's group memberships -4. ✅ Load personal actions via `get_personal_actions(user_id)` -5. ✅ Conditionally merge global actions if merge enabled -6. ❌ **MISSING:** Load group actions from user's group memberships - -### Why It Was Missed -The code had logic to load a **single selected group agent** if explicitly requested, but this was: -- Only triggered when a specific group agent was pre-selected -- Required explicit group ID resolution -- Did not load **all** group agents from user's memberships -- Failed to load group agents proactively for selection - -This created a chicken-and-egg problem: the agent couldn't be selected because it wasn't loaded, and it wasn't loaded unless it was selected. - -## Technical Details - -### Files Modified -1. **`semantic_kernel_loader.py`** (Lines ~1155-1250) - - Added group agent loading after personal agents - - Added group action loading after personal actions - - Removed redundant single-agent loading logic - -2. **`config.py`** (Line 91) - - Updated VERSION from "0.235.026" to "0.235.027" - -### Code Changes - -#### Before (Pseudocode) -```python -agents_cfg = get_personal_agents(user_id) -# Mark personal agents -for agent in agents_cfg: - agent['is_global'] = False - -# Only try to load ONE selected group agent if explicitly requested -if selected_agent_is_group: - # Complex logic to find and add single group agent - -# Merge global agents if enabled -if merge_global: - # Add global agents - -# Load personal actions only -plugin_manifests = get_personal_actions(user_id) -``` - -#### After (Pseudocode) -```python -agents_cfg = get_personal_agents(user_id) -# Mark personal agents -for agent in agents_cfg: - agent['is_global'] = False - agent['is_group'] = False - -# Load ALL group agents from user's group memberships -user_groups = get_user_groups(user_id) -for group in user_groups: - group_agents = get_group_agents(group_id) - for group_agent in group_agents: - # Mark and add to agents_cfg - group_agent['is_global'] = False - group_agent['is_group'] = True - group_agent['group_id'] = group_id - group_agent['group_name'] = group_name - agents_cfg.append(group_agent) - -# Merge global agents if enabled (unchanged) -if merge_global: - # Add global agents - -# Load personal actions -plugin_manifests = get_personal_actions(user_id) - -# Load ALL group actions from user's group memberships -for group in user_groups: - group_actions = get_group_actions(group_id) - plugin_manifests.extend(group_actions) -``` - -### Key Implementation Details - -**Group Agent Loading:** -```python -from functions_group import get_user_groups -from functions_group_agents import get_group_agents - -user_groups = [] # Initialize to empty list -try: - user_groups = get_user_groups(user_id) - print(f"[SK Loader] User '{user_id}' is a member of {len(user_groups)} groups") - - group_agent_count = 0 - for group in user_groups: - group_id = group.get('id') - group_name = group.get('name', 'Unknown') - if group_id: - group_agents = get_group_agents(group_id) - for group_agent in group_agents: - group_agent['is_global'] = False - group_agent['is_group'] = True - group_agent['group_id'] = group_id - group_agent['group_name'] = group_name - agents_cfg.append(group_agent) - group_agent_count += 1 - print(f"[SK Loader] Loaded {len(group_agents)} agents from group '{group_name}' (id: {group_id})") - - if group_agent_count > 0: - log_event(f"[SK Loader] Loaded {group_agent_count} group agents from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) -except Exception as e: - log_event(f"[SK Loader] Error loading group agents for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) - user_groups = [] # Reset to empty on error -``` - -**Group Action Loading:** -```python -# Load group actions from all groups the user is a member of -try: - group_action_count = 0 - for group in user_groups: - group_id = group.get('id') - group_name = group.get('name', 'Unknown') - if group_id: - group_actions = get_group_actions(group_id, return_type=SecretReturnType.NAME) - plugin_manifests.extend(group_actions) - group_action_count += len(group_actions) - print(f"[SK Loader] Loaded {len(group_actions)} actions from group '{group_name}' (id: {group_id})") - - if group_action_count > 0: - log_event(f"[SK Loader] Loaded {group_action_count} group actions from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) -except Exception as e: - log_event(f"[SK Loader] Error loading group actions for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) -``` - -### Functions Used -- **`get_user_groups(user_id)`** - Returns all groups where user is a member (from `functions_group.py`) -- **`get_group_agents(group_id)`** - Returns all agents for a specific group (from `functions_group_agents.py`) -- **`get_group_actions(group_id, return_type)`** - Returns all actions/plugins for a specific group (from `functions_group_actions.py`) - -### Error Handling -- Both group agent and group action loading are wrapped in try-except blocks -- Errors are logged with full exception tracebacks -- On error, `user_groups` is reset to empty list to prevent downstream issues -- System gracefully degrades to personal + global agents if group loading fails - -## Validation - -### Test Scenario -1. **Setup:** - - User `f016493e-9395-4120-91b5-bac4276b6b6c` is member of group `cio6` (ID: `72254e24-4bc6-4680-bc2e-c56d5214d8e8`) - - Group has agent `cio6_servicenow_test_agent` with action `cio6_servicenow_query_incidents` - - Per-user semantic kernel mode enabled - - Global agent merging enabled - -2. **User Action:** - - User selects group agent `cio6_servicenow_test_agent` - - User submits message: "Show me all ServiceNow incidents" - -### Before Fix - Failure Behavior -``` -[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' -[SK Loader] After merging: 3 total agents # Only personal + global -[SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False -[SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent -[SK Loader] selected_agent fallback to first agent: researcher # ❌ Wrong agent -[Enhanced Agent Citations] Extracted 0 detailed plugin invocations # ❌ No actions -{'agent': 'researcher', 'plugin_count': 0} # ❌ Zero plugins -``` - -**Result:** Agent asks clarifying questions instead of querying ServiceNow. - -### After Fix - Success Behavior -``` -[SK Loader] User settings found 1 personal agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' -[SK Loader] User 'f016493e-9395-4120-91b5-bac4276b6b6c' is a member of 1 groups # ✅ Groups detected -[SK Loader] Loaded 1 agents from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group agent loaded -[SK Loader] Loaded 1 group agents from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success -[SK Loader] Total agents loaded: 2 (personal + group) for user 'f016493e-9395-4120-91b5-bac4276b6b6c' -[SK Loader] After merging: 4 total agents # ✅ Includes group agent -[SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False), ('cio6_servicenow_test_agent', False)] # ✅ Group agent present -[SK Loader] Loaded 1 actions from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group action loaded -[SK Loader] Loaded 1 group actions from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success -[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c Found EXACT match for agent: cio6_servicenow_test_agent (is_global=False) # ✅ Agent found -[SK Loader] Plugin cio6_servicenow_query_incidents: SUCCESS # ✅ Plugin loaded -``` - -**Result:** Correct group agent selected with its action available for execution. - -### Verification Checklist -- [x] Personal agents still load correctly -- [x] Global agents still merge correctly when enabled -- [x] Group agents load for all user's group memberships -- [x] Group actions load for all user's group memberships -- [x] Agents properly marked with `is_group` and `group_id` flags -- [x] Agent selection finds group agents by name -- [x] Error handling prevents crashes if group loading fails -- [x] Logging provides visibility into group loading process \ No newline at end of file diff --git a/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md b/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md deleted file mode 100644 index 34eadb4a..00000000 --- a/docs/fixes/OPENAPI_BASIC_AUTH_FIX.md +++ /dev/null @@ -1,205 +0,0 @@ -# OpenAPI Basic Authentication Fix - -**Version:** 0.235.026 -**Issue:** OpenAPI actions with Basic Authentication fail with "session not authenticated" error -**Root Cause:** Mismatch between authentication format stored by UI and format expected by OpenAPI plugin -**Status:** ✅ Fixed - ---- - -## Problem Description - -When configuring an OpenAPI action with Basic Authentication in the Simple Chat admin interface: - -1. User uploads OpenAPI spec with `securitySchemes.basicAuth` defined -2. User selects "Basic Auth" authentication type -3. User enters username and password in the configuration wizard -4. Action is saved successfully -5. **BUT**: When agent attempts to use the action, authentication fails with error: - ``` - "I'm unable to access your ServiceNow incidents because your session - is not authenticated. Please log in to your ServiceNow instance or - check your authentication credentials." - ``` - -### Symptoms -- ❌ OpenAPI actions with Basic Auth fail despite correct credentials -- ✅ Direct API calls with same credentials work correctly -- ✅ Other Simple Chat features authenticate successfully -- ❌ Error occurs even when Base URL is correctly configured - ---- - -## Root Cause Analysis - -### Authentication Storage Format (Frontend) - -The Simple Chat admin UI (`plugin_modal_stepper.js`, lines 1539-1543) stores Basic Auth credentials as: - -```javascript -auth.type = 'key'; // Basic auth is also 'key' type in the schema -const username = document.getElementById('plugin-auth-basic-username').value.trim(); -const password = document.getElementById('plugin-auth-basic-password').value.trim(); -auth.key = `${username}:${password}`; // Store as combined string -additionalFields.auth_method = 'basic'; -``` - -**Stored format:** -```json -{ - "auth": { - "type": "key", - "key": "username:password" - }, - "additionalFields": { - "auth_method": "basic" - } -} -``` - -### Authentication Expected Format (Backend) - -The OpenAPI plugin (`openapi_plugin.py`, lines 952-955) expects Basic Auth as: - -```python -elif auth_type == "basic": - import base64 - username = self.auth.get("username", "") - password = self.auth.get("password", "") - credentials = base64.b64encode(f"{username}:{password}".encode()).decode() - headers["Authorization"] = f"Basic {credentials}" -``` - -**Expected format:** -```json -{ - "auth": { - "type": "basic", - "username": "actual_username", - "password": "actual_password" - } -} -``` - -### The Mismatch - -❌ **Frontend stores:** `auth.type='key'`, `auth.key='username:password'` -❌ **Backend expects:** `auth.type='basic'`, `auth.username`, `auth.password` -❌ **Result:** Plugin code path for Basic Auth (`elif auth_type == "basic"`) never executes -❌ **Consequence:** No `Authorization` header added, API returns authentication error - ---- - -## Solution Implementation - -### Fix Location -**File:** `application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py` -**Function:** `_extract_auth_config()` -**Lines:** 129-166 - -### Code Changes - -Added authentication format transformation logic to detect and convert Simple Chat's storage format into OpenAPI plugin's expected format: - -```python -@classmethod -def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: - """Extract authentication configuration from plugin config.""" - auth_config = config.get('auth', {}) - if not auth_config: - return {} - - auth_type = auth_config.get('type', 'none') - - if auth_type == 'none': - return {} - - # Check if this is basic auth stored in the 'key' field format - # Simple Chat stores basic auth as: auth.type='key', auth.key='username:password', - # additionalFields.auth_method='basic' - additional_fields = config.get('additionalFields', {}) - auth_method = additional_fields.get('auth_method', '') - - if auth_type == 'key' and auth_method == 'basic': - # Extract username and password from the combined key - key = auth_config.get('key', '') - if ':' in key: - username, password = key.split(':', 1) - return { - 'type': 'basic', - 'username': username, - 'password': password - } - else: - # Malformed basic auth key - return {} - - # For bearer tokens stored as 'key' type - if auth_type == 'key' and auth_method == 'bearer': - return { - 'type': 'bearer', - 'token': auth_config.get('key', '') - } - - # For OAuth2 stored as 'key' type - if auth_type == 'key' and auth_method == 'oauth2': - return { - 'type': 'bearer', # OAuth2 tokens are typically bearer tokens - 'token': auth_config.get('key', '') - } - - # Return the auth config as-is for other auth types - return auth_config -``` - -### How It Works - -1. **Detection:** Check if `auth.type == 'key'` AND `additionalFields.auth_method == 'basic'` -2. **Extraction:** Split `auth.key` on first `:` to get username and password -3. **Transformation:** Return new dict with `type='basic'`, `username`, and `password` -4. **Pass-through:** OpenAPI plugin receives correct format and adds Authorization header - -### Additional Auth Method Support - -The fix also handles other authentication methods stored in the same format: -- **Bearer tokens:** `auth_method='bearer'` → transforms to `{type: 'bearer', token: ...}` -- **OAuth2:** `auth_method='oauth2'` → transforms to `{type: 'bearer', token: ...}` - ---- - -## Testing - -### Before Fix -```bash -# Test action: servicenow_query_incidents -User: "Show me all incidents in ServiceNow" -Agent: "I'm unable to access your ServiceNow incidents because your - session is not authenticated..." - -# HTTP request (no Authorization header sent): -GET https://dev222288.service-now.com/api/now/table/incident -# Response: 401 Unauthorized or session expired error -``` - -### After Fix -```bash -# Test action: servicenow_query_incidents -User: "Show me all incidents in ServiceNow" -Agent: "Here are your ServiceNow incidents: ..." - -# HTTP request (Authorization header correctly added): -GET https://dev222288.service-now.com/api/now/table/incident -Authorization: Basic - -# Response: 200 OK with incident data -``` - -### Validation Steps -1. ✅ Create OpenAPI action with Basic Auth -2. ✅ Enter username and password in admin wizard -3. ✅ Save action successfully -4. ✅ Attach action to agent -5. ✅ Test agent with prompt requiring action -6. ✅ Verify Authorization header is sent -7. ✅ Verify API returns 200 OK with data -8. ✅ Verify agent processes response correctly \ No newline at end of file From 33bee688e831e825d1b4131c99f654569eaa1d2a Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 19:22:13 -0500 Subject: [PATCH 04/35] Updated servicenow integration readme --- docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md index 66e5a983..4d400961 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -183,6 +183,15 @@ The integration uses two OpenAPI specification files that define all ServiceNow ## Phase 3: Simple Chat Configuration +> **📌 Important - Scope Options:** +> ServiceNow actions and agents can be configured at different levels based on your organization's needs: +> +> - **Global Actions/Agents**: Available to all users across the entire Simple Chat instance +> - **Group Actions/Agents**: Available only to members of specific workspaces/groups +> - **Personal Actions/Agents**: Available only to individual users +> +> Choose the appropriate scope based on your security, governance, and access control requirements. For enterprise deployments, group-level configuration is recommended to control access by department or team. + ### Step 1: Add ServiceNow Actions > **Note:** This integration uses **two separate actions** because ServiceNow has distinct API endpoints for incident management and knowledge base operations, each with its own OpenAPI specification file. From cd8c520d5161037ee9d7e8e0d28ba9cda9b9a240 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 19:46:52 -0500 Subject: [PATCH 05/35] chore: Revert custom logo changes to upstream version --- .../single_app/static/images/custom_logo.png | Bin 7586 -> 11705 bytes .../static/images/custom_logo_dark.png | Bin 0 -> 13770 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 application/single_app/static/images/custom_logo_dark.png diff --git a/application/single_app/static/images/custom_logo.png b/application/single_app/static/images/custom_logo.png index ab32f65fe249cf825c6b6e2f464bb3da11a73eba..45a99fd35f8834db8920ea29bd2bfee10fe754d2 100644 GIT binary patch literal 11705 zcmbVSRa6~Iv>XoZ2Pe23+%-UOcXyW%G`Ks#NpK79?(P!Yf`s56+}-VQKi=p2d27w= z{@T5I?Vg^ls$CJvic+XZ1V{h?pvp*#tG>xcAr>>W>TnOaU_DBI+I)C%)bp z)DoVjSDG5`{x84BCr;~*>b_9INPx|uba42cuEGbiKVs_SN6>^?j;s7*^5Ty()1Si6 zmsAq{v1{tYzgx`qFKQ!1j-o=65^h`CMS^@d4aZIAmc5?i%Al=0>q<()^-kaY3%
txT=*o8&jKB+g$Gzp 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 Kc$KK}xBmg`z(eK$ literal 7586 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 literal 0 HcmV?d00001 From fb8181bcfa0446ffed7f09a02938084258c26d33 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 19:52:43 -0500 Subject: [PATCH 06/35] chore: Revert terraform main.tf to upstream version --- deployers/terraform/main.tf | 71 ++++++++----------------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index 12029506..77b486df 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -172,7 +172,6 @@ locals { cosmos_db_name = "${var.param_base_name}-${var.param_environment}-cosmos" open_ai_name = "${var.param_base_name}-${var.param_environment}-oai" doc_intel_name = "${var.param_base_name}-${var.param_environment}-docintel" - speech_service_name = "${var.param_base_name}-${var.param_environment}-speech" key_vault_name = "${var.param_base_name}-${var.param_environment}-kv" log_analytics_name = "${var.param_base_name}-${var.param_environment}-la" managed_identity_name = "${var.param_base_name}-${var.param_environment}-id" @@ -626,14 +625,13 @@ resource "azurerm_cosmosdb_account" "cosmos" { # --- Azure OpenAI Service (Cognitive Services) --- resource "azurerm_cognitive_account" "openai" { - count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing - name = local.open_ai_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "OpenAI" - sku_name = "S0" # Standard tier - custom_subdomain_name = local.open_ai_name # Required for managed identity authentication - tags = local.common_tags + count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing + name = local.open_ai_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "OpenAI" + sku_name = "S0" # Standard tier + tags = local.common_tags } # Data source for existing OpenAI instance @@ -645,24 +643,13 @@ data "azurerm_cognitive_account" "existing_openai" { # --- Document Intelligence Service (Cognitive Services) --- resource "azurerm_cognitive_account" "docintel" { - name = local.doc_intel_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "FormRecognizer" - sku_name = "S0" # Standard tier - custom_subdomain_name = local.doc_intel_name # Required for managed identity authentication - tags = local.common_tags -} - -# --- Speech Service (Cognitive Services) --- -resource "azurerm_cognitive_account" "speech" { - name = local.speech_service_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "SpeechServices" - sku_name = "S0" # Standard tier - custom_subdomain_name = local.speech_service_name # Required for managed identity authentication - tags = local.common_tags + name = local.doc_intel_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "FormRecognizer" + sku_name = "S0" # Standard tier + custom_subdomain_name = local.doc_intel_name # Maps to --custom-domain + tags = local.common_tags } # https://medium.com/expert-thinking/mastering-azure-search-with-terraform-a-how-to-guide-7edc3a6b1ee3 @@ -715,20 +702,6 @@ resource "azurerm_role_assignment" "managed_identity_storage_contributor" { principal_id = azurerm_user_assigned_identity.id.principal_id } -# Cognitive Services Speech User on Speech Service -resource "azurerm_role_assignment" "managed_identity_speech_user" { - scope = azurerm_cognitive_account.speech.id - role_definition_name = "Cognitive Services Speech User" - principal_id = azurerm_user_assigned_identity.id.principal_id -} - -# Cognitive Services Speech Contributor on Speech Service -resource "azurerm_role_assignment" "managed_identity_speech_contributor" { - scope = azurerm_cognitive_account.speech.id - role_definition_name = "Cognitive Services Speech Contributor" - principal_id = azurerm_user_assigned_identity.id.principal_id -} - # App Registration Service Principal RBAC # Cognitive Services OpenAI Contributor on OpenAI resource "azurerm_role_assignment" "app_reg_sp_openai_contributor" { @@ -759,27 +732,13 @@ resource "azurerm_role_assignment" "app_service_smi_storage_contributor" { principal_id = azurerm_linux_web_app.app.identity[0].principal_id } -# AcrPull on Container Registry +# Storage Blob Data Contributor on Storage Account resource "azurerm_role_assignment" "acr_pull" { scope = data.azurerm_container_registry.acrregistry.id role_definition_name = "AcrPull" principal_id = azurerm_linux_web_app.app.identity[0].principal_id } -# Cognitive Services Speech User on Speech Service -resource "azurerm_role_assignment" "app_service_smi_speech_user" { - scope = azurerm_cognitive_account.speech.id - role_definition_name = "Cognitive Services Speech User" - principal_id = azurerm_linux_web_app.app.identity[0].principal_id -} - -# Cognitive Services Speech Contributor on Speech Service -resource "azurerm_role_assignment" "app_service_smi_speech_contributor" { - scope = azurerm_cognitive_account.speech.id - role_definition_name = "Cognitive Services Speech Contributor" - principal_id = azurerm_linux_web_app.app.identity[0].principal_id -} - ################################################## # From 660d76c0524f6e79f0ae3cc3c40660c061c47fb5 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 19:57:38 -0500 Subject: [PATCH 07/35] Removed the two openai sample spec downloaed from servicennow site --- .../now_knowledge_latest_spec_sample.yaml | 33 -- .../now_table_api_latest_spec_sample.yaml | 331 ------------------ 2 files changed, 364 deletions(-) delete mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml delete mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml deleted file mode 100644 index 4932e7ab..00000000 --- a/docs/how-to/agents/ServiceNow/open_api_specs/now_knowledge_latest_spec_sample.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -openapi: "3.0.1" -info: - title: "Knowledge" - description: "Knowledge APIs for Service Portal" - version: "latest" -externalDocs: - url: "" -servers: -- url: "https://dev222288.service-now.com/" -paths: - /api/now/knowledge/search/facets: - get: - description: "" - parameters: [] - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - post: - description: "" - parameters: [] - requestBody: - content: - application/json: {} - responses: - "200": - description: "ok" - content: - application/json: {} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml deleted file mode 100644 index 3aaaf7f7..00000000 --- a/docs/how-to/agents/ServiceNow/open_api_specs/now_table_api_latest_spec_sample.yaml +++ /dev/null @@ -1,331 +0,0 @@ ---- -openapi: "3.0.1" -info: - title: "Table API" - description: "Allows you to perform create, read, update and delete (CRUD) operations\ - \ on existing tables" - version: "latest" -externalDocs: - url: "https://docs.servicenow.com/?context=CSHelp:REST-Table-API" -servers: -- url: "https://dev222288.service-now.com/" -paths: - /api/now/table/{tableName}: - get: - description: "Retrieve records from a table" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sysparm_query" - in: "query" - description: "An encoded query string used to filter the results" - required: false - schema: {} - - name: "sysparm_display_value" - in: "query" - description: "Return field display values (true), actual values (false), or\ - \ both (all) (default: false)" - required: false - schema: {} - - name: "sysparm_exclude_reference_link" - in: "query" - description: "True to exclude Table API links for reference fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_suppress_pagination_header" - in: "query" - description: "True to supress pagination header (default: false)" - required: false - schema: {} - - name: "sysparm_fields" - in: "query" - description: "A comma-separated list of fields to return in the response" - required: false - schema: {} - - name: "sysparm_limit" - in: "query" - description: "The maximum number of results returned per page (default: 10,000)" - required: false - schema: {} - - name: "sysparm_view" - in: "query" - description: "Render the response according to the specified UI view (overridden\ - \ by sysparm_fields)" - required: false - schema: {} - - name: "sysparm_query_category" - in: "query" - description: "Name of the query category (read replica category) to use for\ - \ queries" - required: false - schema: {} - - name: "sysparm_query_no_domain" - in: "query" - description: "True to access data across domains if authorized (default: false)" - required: false - schema: {} - - name: "sysparm_no_count" - in: "query" - description: "Do not execute a select count(*) on table (default: false)" - required: false - schema: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - post: - description: "Create a record" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sysparm_display_value" - in: "query" - description: "Return field display values (true), actual values (false), or\ - \ both (all) (default: false)" - required: false - schema: {} - - name: "sysparm_exclude_reference_link" - in: "query" - description: "True to exclude Table API links for reference fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_fields" - in: "query" - description: "A comma-separated list of fields to return in the response" - required: false - schema: {} - - name: "sysparm_input_display_value" - in: "query" - description: "Set field values using their display value (true) or actual\ - \ value (false) (default: false)" - required: false - schema: {} - - name: "sysparm_suppress_auto_sys_field" - in: "query" - description: "True to suppress auto generation of system fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_view" - in: "query" - description: "Render the response according to the specified UI view (overridden\ - \ by sysparm_fields)" - required: false - schema: {} - requestBody: - content: - application/json: {} - application/xml: {} - text/xml: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - /api/now/table/{tableName}/{sys_id}: - get: - description: "Retrieve a record" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sys_id" - in: "path" - required: true - schema: {} - - name: "sysparm_display_value" - in: "query" - description: "Return field display values (true), actual values (false), or\ - \ both (all) (default: false)" - required: false - schema: {} - - name: "sysparm_exclude_reference_link" - in: "query" - description: "True to exclude Table API links for reference fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_fields" - in: "query" - description: "A comma-separated list of fields to return in the response" - required: false - schema: {} - - name: "sysparm_view" - in: "query" - description: "Render the response according to the specified UI view (overridden\ - \ by sysparm_fields)" - required: false - schema: {} - - name: "sysparm_query_no_domain" - in: "query" - description: "True to access data across domains if authorized (default: false) " - required: false - schema: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - put: - description: "Modify a record" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sys_id" - in: "path" - required: true - schema: {} - - name: "sysparm_display_value" - in: "query" - description: "Return field display values (true), actual values (false), or\ - \ both (all) (default: false)" - required: false - schema: {} - - name: "sysparm_exclude_reference_link" - in: "query" - description: "True to exclude Table API links for reference fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_fields" - in: "query" - description: "A comma-separated list of fields to return in the response" - required: false - schema: {} - - name: "sysparm_input_display_value" - in: "query" - description: "Set field values using their display value (true) or actual\ - \ value (false) (default: false)" - required: false - schema: {} - - name: "sysparm_suppress_auto_sys_field" - in: "query" - description: "True to suppress auto generation of system fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_view" - in: "query" - description: "Render the response according to the specified UI view (overridden\ - \ by sysparm_fields)" - required: false - schema: {} - - name: "sysparm_query_no_domain" - in: "query" - description: "True to access data across domains if authorized (default: false)" - required: false - schema: {} - requestBody: - content: - application/json: {} - application/xml: {} - text/xml: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - delete: - description: "Delete a record" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sys_id" - in: "path" - required: true - schema: {} - - name: "sysparm_query_no_domain" - in: "query" - description: "True to access data across domains if authorized (default: false)" - required: false - schema: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} - patch: - description: "Update a record" - parameters: - - name: "tableName" - in: "path" - required: true - schema: {} - - name: "sys_id" - in: "path" - required: true - schema: {} - - name: "sysparm_display_value" - in: "query" - description: "Return field display values (true), actual values (false), or\ - \ both (all) (default: false)" - required: false - schema: {} - - name: "sysparm_exclude_reference_link" - in: "query" - description: "True to exclude Table API links for reference fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_fields" - in: "query" - description: "A comma-separated list of fields to return in the response" - required: false - schema: {} - - name: "sysparm_input_display_value" - in: "query" - description: "Set field values using their display value (true) or actual\ - \ value (false) (default: false)" - required: false - schema: {} - - name: "sysparm_suppress_auto_sys_field" - in: "query" - description: "True to suppress auto generation of system fields (default:\ - \ false)" - required: false - schema: {} - - name: "sysparm_view" - in: "query" - description: "Render the response according to the specified UI view (overridden\ - \ by sysparm_fields)" - required: false - schema: {} - - name: "sysparm_query_no_domain" - in: "query" - description: "True to access data across domains if authorized (default: false)" - required: false - schema: {} - requestBody: - content: - application/json: {} - application/xml: {} - text/xml: {} - responses: - "200": - description: "ok" - content: - application/json: {} - application/xml: {} - text/xml: {} From de866eb4fd93820a6c4b5fc38d97810aa22a594b Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:00:05 -0500 Subject: [PATCH 08/35] Update docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agents/ServiceNow/servicenow_agent_instructions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt index fb0aad8e..cf181939 100644 --- a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt +++ b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt @@ -14,14 +14,14 @@ You are a ServiceNow support specialist with direct API access. Execute actions **You have these operations - use them immediately:** -From "cio6 ServiceNow - Manage Incidents" action: +From the ServiceNow incident management action (Manage Incidents): - queryIncidents - Query/filter incidents with advanced search - createIncident - Create new incidents with all fields - getIncidentDetails - Retrieve full incident details by sys_id - updateIncident - Update incident state, assignments, work notes, etc. - getIncidentStats - Get aggregated statistics and metrics -From "cio6 ServiceNow - Search Knowledge Base" action: +From the ServiceNow knowledge base search action (Search Knowledge Base): - searchKnowledgeFacets - Search KB articles with progressive search - getKnowledgeArticle - Retrieve complete article content by sys_id From 20f994a65d58b56ba4743410a19843ba8fbb7184 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:00:44 -0500 Subject: [PATCH 09/35] Update docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../open_api_specs/sample_servicenow_incident_api.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml index f12b8305..702514e7 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api.yaml @@ -8,8 +8,8 @@ info: url: https://developer.servicenow.com servers: - - url: https://dev222288.service-now.com/api/now - description: ServiceNow Developer Instance + - url: https://YOUR-INSTANCE.service-now.com/api/now + description: ServiceNow instance base URL (replace YOUR-INSTANCE with your instance name) security: - bearerAuth: [] From 351f143ca5553bed1c3884a04dd8e76378c1e20e Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:01:17 -0500 Subject: [PATCH 10/35] Update docs/how-to/azure_speech_managed_identity_manul_setup.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/how-to/azure_speech_managed_identity_manul_setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/azure_speech_managed_identity_manul_setup.md b/docs/how-to/azure_speech_managed_identity_manul_setup.md index 7941542d..bf1b6e74 100644 --- a/docs/how-to/azure_speech_managed_identity_manul_setup.md +++ b/docs/how-to/azure_speech_managed_identity_manul_setup.md @@ -1,4 +1,4 @@ -# Azure Speech Service with Managed Identity Setup +# Azure Speech Service with Managed Identity Manual Setup ## Overview From 5fe5b1497c5f8dc0e47d4abf4cf7beea0b10fe51 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:01:55 -0500 Subject: [PATCH 11/35] Update application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../semantic_kernel_plugins/openapi_plugin_factory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py index 8f8a4d84..b22a1d53 100644 --- a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py +++ b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py @@ -162,7 +162,10 @@ def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: # For bearer tokens stored as 'key' type if auth_type == 'key' and auth_method == 'bearer': token = auth_config.get('key', '') - debug_print(f"[Factory] *** APPLYING BEARER AUTH TRANSFORMATION - token: {token[:20]}...") + debug_print( + f"[Factory] Applying bearer auth transformation - " + f"token_present={bool(token)}, token_length={len(token)}" + ) return { 'type': 'bearer', 'token': token From 96262193820647f9cf9f04f956be074a24a4a76b Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:08:11 -0500 Subject: [PATCH 12/35] Checked in the bug fix detail readme to docs/explanation/fixes/v0.236.012 --- .../openapi_plugin_factory.py | 2 +- .../AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 226 ++++++++++ .../GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 403 ++++++++++++++++++ .../v0.236.012/GROUP_AGENT_LOADING_FIX.md | 241 +++++++++++ .../v0.236.012/OPENAPI_BASIC_AUTH_FIX.md | 205 +++++++++ 5 files changed, 1076 insertions(+), 1 deletion(-) create mode 100644 docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md create mode 100644 docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md create mode 100644 docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md create mode 100644 docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md diff --git a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py index 8f8a4d84..3194b7b7 100644 --- a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py +++ b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py @@ -162,7 +162,7 @@ def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: # For bearer tokens stored as 'key' type if auth_type == 'key' and auth_method == 'bearer': token = auth_config.get('key', '') - debug_print(f"[Factory] *** APPLYING BEARER AUTH TRANSFORMATION - token: {token[:20]}...") + debug_print(f"[Factory] Applying basic auth transformation") return { 'type': 'bearer', 'token': token diff --git a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md new file mode 100644 index 00000000..0d507ecc --- /dev/null +++ b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md @@ -0,0 +1,226 @@ +# Azure AI Search Test Connection Fix + +## Issue Description + +When clicking the "Test Azure AI Search Connection" button on the App Settings "Search & Extract" page with **managed identity authentication** enabled, the test connection failed with the following error: + +**Original Error Message:** +``` +NameError: name 'search_resource_manager' is not defined +``` + +**Environment Configuration:** +- Authentication Type: Managed Identity +- Azure Environment: `public` (set in .env file) +- Error occurred because the code tried to reference `search_resource_manager` which wasn't defined for public cloud + +**Root Cause:** The old implementation used a REST API approach that required the `search_resource_manager` variable to construct authentication tokens. This variable wasn't defined for the public cloud environment, causing the initial error. Even if defined, the REST API approach with bearer tokens doesn't work properly with Azure AI Search's managed identity authentication. + +## Root Cause Analysis + +The old implementation used a **REST API approach with manually acquired bearer tokens**, which is fundamentally incompatible with how Azure AI Search handles managed identity authentication on the data plane. + +### Why the Old Approach Failed + +Azure AI Search's data plane operations don't properly accept bearer tokens acquired through standard `DefaultAzureCredential.get_token()` flows and passed as HTTP Authorization headers. The authentication mechanism works differently: + +```python +# OLD IMPLEMENTATION - FAILED ❌ +credential = DefaultAzureCredential() +arm_scope = f"{search_resource_manager}/.default" +token = credential.get_token(arm_scope).token + +headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" +} +response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) +# Returns: 403 Forbidden +``` + +**Problems with this approach:** +1. Azure AI Search requires SDK-specific authentication handling +2. Bearer tokens from `get_token()` are rejected by the Search service +3. Token scope and refresh logic need specialized handling +4. This issue occurs in **all Azure environments** (public, government, custom) + +### Why Other Services Work with REST API + Bearer Tokens + +Some Azure services accept bearer tokens in REST API calls, but Azure AI Search requires the SDK to: +1. Acquire tokens using the correct scope and flow +2. Handle token refresh automatically +3. Use Search-specific authentication headers +4. Properly negotiate with the Search service's auth layer + +## Technical Details + +### Files Modified + +**File:** `route_backend_settings.py` +**Function:** `_test_azure_ai_search_connection(payload)` +**Lines:** 760-796 + +### The Solution + +Instead of trying to define `search_resource_manager` for public cloud, the fix was to **replace the REST API approach entirely with the SearchIndexClient SDK**, which handles authentication correctly without needing the `search_resource_manager` variable. + +### Code Changes Summary + +**Before (REST API approach):** +```python +def _test_azure_ai_search_connection(payload): + # ... setup code ... + + if direct_data.get('auth_type') == 'managed_identity': + credential = DefaultAzureCredential() + arm_scope = f"{search_resource_manager}/.default" + token = credential.get_token(arm_scope).token + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) + # ❌ Returns 403 Forbidden +``` + +**After (SDK approach):** +```python +def _test_azure_ai_search_connection(payload): + # ... setup code ... + + if direct_data.get('auth_type') == 'managed_identity': + credential = DefaultAzureCredential() + + # Use SDK which handles authentication properly + if AZURE_ENVIRONMENT in ("usgovernment", "custom"): + client = SearchIndexClient( + endpoint=endpoint, + credential=credential, + audience=search_resource_manager + ) + else: + # For public cloud, don't use audience parameter + client = SearchIndexClient( + endpoint=endpoint, + credential=credential + ) + + # Test by listing indexes (simple operation to verify connectivity) + indexes = list(client.list_indexes()) + # ✅ Works correctly +``` + +### Key Implementation Details + +1. **Replaced REST API with SearchIndexClient SDK** + - Uses `SearchIndexClient` from `azure.search.documents` + - SDK handles authentication internally + - Properly manages token acquisition and refresh + +2. **Environment-Specific Configuration** + - **Azure Government/Custom:** Requires `audience` parameter + - **Azure Public Cloud:** Omits `audience` parameter + - Matches pattern used throughout codebase + +3. **Consistent with Other Functions** + - Aligns with `get_index_client()` implementation (line 484) + - Matches SearchClient initialization in `config.py` (lines 584-619) + - All other search operations already use SDK approach + +## Testing Approach + +### Prerequisites +- Service principal must have **"Search Index Data Contributor"** RBAC role +- Permissions must propagate (5-10 minutes after assignment) + +### RBAC Role Assignment Command +```bash +az role assignment create \ + --assignee \ + --role "Search Index Data Contributor" \ + --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ +``` + +### Verification +```bash +az role assignment list \ + --assignee \ + --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ \ + --output table +``` + +## Impact Analysis + +### What Changed +- **Only the test connection function** was affected +- No changes needed to actual search operations (indexing, querying, etc.) +- All other search functionality already used correct SDK approach + +### Why Other Search Operations Weren't Affected +All production search operations throughout the codebase already use the SDK: +- `SearchClient` for querying indexes +- `SearchIndexClient` for managing indexes +- `get_index_client()` helper function +- Index initialization in `config.py` + +**Only the test connection function used the failed REST API approach.** + +## Validation + +### Before Fix +- ✅ Authentication succeeded (no credential errors) +- ✅ Token acquisition worked +- ❌ Azure AI Search rejected bearer token (403 Forbidden) +- ❌ Test connection failed + +### After Fix +- ✅ Authentication succeeds +- ✅ SDK handles token acquisition properly +- ✅ Azure AI Search accepts SDK authentication +- ✅ Test connection succeeds (with proper RBAC permissions) + +## Configuration Requirements + +### Public Cloud (.env) +```ini +AZURE_ENVIRONMENT=public +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_TENANT_ID= +AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity +AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.net +``` + +### Azure Government (.env) +```ini +AZURE_ENVIRONMENT=usgovernment +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_TENANT_ID= +AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity +AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.us +``` + +## Related Changes + +**No config.py changes were made.** The fix was entirely in the route_backend_settings.py file by switching from REST API to SDK approach. + +The SDK approach eliminates the need for the `search_resource_manager` variable in public cloud because: +- The SearchIndexClient handles authentication internally +- No manual token acquisition is needed +- The SDK knows the correct endpoints and scopes automatically + +## Version Information + +**Version Implemented:** 0.235.004 + +## References + +- Azure AI Search SDK Documentation: https://learn.microsoft.com/python/api/azure-search-documents +- Azure RBAC for Search: https://learn.microsoft.com/azure/search/search-security-rbac +- DefaultAzureCredential: https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential + +## Summary + +The fix replaces manual REST API calls with the proper Azure Search SDK (`SearchIndexClient`), which correctly handles managed identity authentication for Azure AI Search. This aligns the test function with all other search operations in the codebase that already use the SDK approach successfully. diff --git a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md new file mode 100644 index 00000000..196bc132 --- /dev/null +++ b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -0,0 +1,403 @@ +# Group Action OAuth Authentication and Schema Merging Fix + +## Header Information + +**Fix Title:** Group Actions Missing `additionalFields` Causing OAuth Authentication Failures +**Issue Description:** Group actions were missing the `additionalFields` property entirely, preventing OAuth bearer token authentication from working despite having the same configuration as working global actions. +**Root Cause:** Group action backend routes did not call `get_merged_plugin_settings()` to merge UI form data with schema defaults, while global action routes did. This caused group actions to be saved without authentication configuration fields. +**Version Implemented:** 0.235.028 +**Date:** January 22, 2026 + +## Problem Statement + +### Symptoms +When a group action was configured with OAuth bearer token authentication: +- Action execution returned **HTTP 401 Unauthorized** errors +- ServiceNow API responded: `{"error":{"message":"User is not authenticated"}}` +- UI displayed `additionalFields: {}` (empty object) when editing group action +- Global action with identical configuration showed populated `additionalFields` and worked correctly +- Bearer token header was not being sent in API requests + +### Impact +- **Severity:** High - OAuth authentication completely non-functional for group actions +- **Affected Users:** All users attempting to use group actions with OAuth/Bearer token authentication +- **Workaround:** Use global actions instead of group actions (not scalable) + +### Evidence from Logs +``` +[DEBUG] Auth type: bearer +[DEBUG] Token available: True +[DEBUG] Added bearer auth: EfP7otqXmV... +[DEBUG] Making request to https://dev222288.service-now.com/api/now/table/incident +[DEBUG] Request headers: {'Authorization': 'Bearer EfP7otqXmV...', ...} +[DEBUG] Response status: 401 +[DEBUG] Response text: {"error":{"message":"User is not authenticated",...}} +``` + +**Critical Discovery:** When comparing global vs group action data: +- **Global action** (working): `additionalFields: {auth_method: 'bearer', base_url: '...', ...}` +- **Group action** (failing): `additionalFields: {}` ← Empty object! + +## Root Cause Analysis + +### Backend Route Disparity + +#### Global Action Routes (Working) +**File:** `route_backend_plugins.py` - Lines 666-667 (add_plugin route) + +```python +# Global action creation route +merged = get_merged_plugin_settings( + plugin_type, + current_settings=additionalFields, + schema_dir=schema_dir +) +``` + +**Result:** UI form data is merged with schema defaults, preserving authentication configuration sent from JavaScript. + +#### Group Action Routes (Broken - Before Fix) +**File:** `route_backend_plugins.py` - Lines 430-470, 485-530 + +```python +# Group action creation/update routes - BEFORE FIX +# NO CALL to get_merged_plugin_settings() +# additionalFields saved directly from request without merging +``` + +**Result:** `additionalFields` data from UI was not being preserved, resulting in empty objects. + +### Data Flow Architecture + +The fix revealed the actual data flow for authentication configuration: + +1. **UI Layer** (`plugin_modal_stepper.js` line 1537): + ```javascript + additionalFields.auth_method = 'bearer'; // Set by JavaScript based on dropdown + ``` + +2. **HTTP POST** to backend: + ```json + { + "name": "action_name", + "auth": {"type": "key"}, + "additionalFields": { + "auth_method": "bearer", + "base_url": "https://dev222288.service-now.com/api/now" + } + } + ``` + +3. **Backend Processing** - `get_merged_plugin_settings()`: + - **If schema file exists:** Merge UI data with schema defaults + - **If schema file missing:** Return UI data unchanged (graceful fallback) + - **If function not called:** Data lost! + +4. **Storage:** Cosmos DB saves merged data + +### Why Global Actions Worked Without Schema File + +**Key Insight:** The `openapi_plugin.additional_settings.schema.json` file **never existed** for global actions either! + +Global actions worked because: +1. Backend routes **called** `get_merged_plugin_settings()` +2. Function detected missing schema file +3. **Graceful fallback** (lines 110-114 in `functions_plugins.py`): + ```python + else: + result[nested_key] = current_val # Return UI data unchanged + ``` +4. UI data passed through and was saved correctly + +Group actions failed because: +1. Backend routes **did not call** the merge function at all +2. `additionalFields` from UI was discarded +3. Empty object `{}` saved to database +4. OAuth configuration lost + +## Technical Details + +### Files Modified + +1. **`route_backend_plugins.py`** (Lines 430-530) + - **Line 461-463** (create_group_action_route): Added schema merging + - **Line 520-522** (update_group_action_route): Added schema merging + - **Parity achieved:** Both global and group routes now call `get_merged_plugin_settings()` + +2. **`config.py`** + - Updated VERSION from "0.235.027" to "0.235.028" + +### Code Changes + +#### Group Action Creation Route - BEFORE +```python +def create_group_action_route(user_id, group_id): + """Create new group action""" + data = request.get_json() + # ... validation ... + + # Direct save without merging + saved_plugin = save_group_action( + user_id=user_id, + group_id=group_id, + plugin_data=data # additionalFields lost here! + ) +``` + +#### Group Action Creation Route - AFTER (Fixed) +```python +def create_group_action_route(user_id, group_id): + """Create new group action""" + data = request.get_json() + # ... validation ... + + # NEW: Merge additionalFields with schema defaults (lines 461-463) + merged = get_merged_plugin_settings( + plugin_type=data.get('type', 'openapi'), + current_settings=data.get('additionalFields', {}), + schema_dir=schema_dir + ) + data['additionalFields'] = merged + + saved_plugin = save_group_action( + user_id=user_id, + group_id=group_id, + plugin_data=data # Now includes preserved auth config! + ) +``` + +**Same fix applied to:** +- `update_group_action_route()` (lines 520-522) + +### Graceful Fallback Behavior + +**File:** `functions_plugins.py` (Lines 92-115) + +```python +def get_merged_plugin_settings(plugin_type, current_settings, schema_dir): + """ + Merge plugin settings with schema defaults. + + If schema file doesn't exist: returns current_settings unchanged. + This is intentional - allows UI-driven configuration. + """ + schema_path = os.path.join(schema_dir, f"{plugin_type}.additional_settings.schema.json") + + if not os.path.exists(schema_path): + # Graceful fallback - return UI data as-is (lines 110-114) + result = {} + for nested_key in current_settings: + result[nested_key] = current_settings[nested_key] # Preserve UI data + return result + + # If schema exists, merge with defaults + # ... +``` + +**Design Decision:** Schema files are **optional** - the system works perfectly with UI-driven configuration via graceful fallback. + +## Solution Implemented + +### Fix Strategy +1. ✅ Add `get_merged_plugin_settings()` calls to group action routes (parity with global routes) +2. ✅ Rely on UI-driven configuration + backend graceful fallback (proven approach) +3. ✅ Require recreation of existing group actions to populate `additionalFields` + +### Architecture Result + +**Both global and group routes now have identical behavior:** + +1. **UI sends complete `additionalFields`** from form +2. **Backend calls `get_merged_plugin_settings()`** for parity +3. **Function detects no schema file** exists +4. **Graceful fallback returns UI data unchanged** +5. **Complete authentication config saved** to database + +**Benefits:** +- ✅ Simple: UI drives configuration, backend preserves it +- ✅ Proven: Global actions validate this approach +- ✅ Maintainable: No schema files to keep in sync +- ✅ Flexible: Easy to extend authentication types in UI + +## Validation + +### Test Procedure +1. Delete existing group action (has empty `additionalFields`) +2. Create new group action via UI: + - Type: OpenAPI + - Upload ServiceNow spec + - Base URL: `https://dev222288.service-now.com/api/now` + - Authentication: **Bearer Token** (dropdown selection) + - Token: `EfP7otqXmVmg06xfB9igagxL6Pjir7ewv99sZyMqYdzImlerPt9rHM1T1_L8cCEeWZAuWUV0GPDP2eZ56XWoEQ` +3. UI JavaScript sets `additionalFields.auth_method = 'bearer'` (line 1537) +4. Backend merge function preserves UI data via fallback +5. Action saved with complete authentication configuration + +### Expected Results +- ✅ Group action `additionalFields` populated: `{auth_method: 'bearer', base_url: '...', ...}` +- ✅ ServiceNow API calls return **HTTP 200** instead of 401 +- ✅ Authorization header sent: `Bearer EfP7otqXmV...` +- ✅ Group agent successfully queries ServiceNow incidents +- ✅ Edit group action page displays authentication fields correctly + +## Impact Analysis + +### Before Fix +- **Global actions:** ✅ Working - routes call merge function +- **Group actions:** ❌ Broken - routes don't call merge function +- **Result:** OAuth authentication impossible for group actions + +### After Fix +- **Global actions:** ✅ Working - routes call merge function → fallback preserves UI data +- **Group actions:** ✅ Working - routes call merge function → fallback preserves UI data +- **Result:** Complete parity, OAuth authentication works for both + +### Breaking Changes +**None** - This is a pure fix with backward compatibility: +- Existing global actions continue working (unchanged code path) +- **New/recreated** group actions now work correctly +- Existing broken group actions remain broken until recreated (user action required) + +## Lessons Learned + +### Key Insights +1. **UI is source of truth for authentication config** - Backend preserves what UI sends +2. **Graceful fallback is a feature, not a bug** - Enables UI-driven configuration +3. **Code parity prevents subtle bugs** - Global and group routes should be identical +4. **Testing existing functionality reveals architecture** - Global actions proved UI approach works + +### Best Practices Reinforced +- **Investigate working code before making changes** - Global actions showed the pattern +- **Prefer simplicity** - UI-driven configuration simpler than complex schema systems +- **Document data flows** - Understanding UI → Backend → DB flow was crucial +- **Test parity** - If code paths differ, investigate why + +## Related Documentation +- **[Group Agent Loading Fix](./GROUP_AGENT_LOADING_FIX.md)** - Prerequisites for this fix (v0.235.027) +- **ServiceNow OAuth Setup** - Configuration instructions for OAuth 2.0 bearer tokens +- **Plugin Modal Stepper** - UI component responsible for authentication form (`plugin_modal_stepper.js`) + +## Future Considerations + +### ⚠️ CRITICAL: OAuth 2.0 Token Expiration Limitation + +**Current Implementation Status:** +- ✅ **Bearer token authentication works correctly** - tokens are sent properly in HTTP headers +- ❌ **No automatic token refresh** - requires manual regeneration when expired +- ⚠️ **Production limitation** - not suitable for production use without enhancement + +**The Problem:** +ServiceNow OAuth access tokens expire after a configured lifespan (e.g., 3,600 seconds = 1 hour). The current Simple Chat implementation: + +1. **Stores static bearer tokens** - copied from ServiceNow and hardcoded in action configuration +2. **No expiration tracking** - doesn't know when token will expire +3. **No refresh mechanism** - can't automatically request new tokens +4. **Manual workaround required** - users must regenerate and update token every hour + +**Example Failure:** +``` +Request: GET https://dev222288.service-now.com/api/now/table/incident +Headers: Authorization: Bearer EfP7otqXmV... (expired token) +Response: HTTP 401 - {"error":{"message":"User is not authenticated"}} +``` + +**Temporary Testing Workaround:** +- Increase ServiceNow "Access Token Lifespan" to longer duration (e.g., 86,400 seconds = 24 hours) +- Regenerate token before expiration +- **Not suitable for production environments** + +**Proper Solution Required (Future Enhancement):** + +To make OAuth 2.0 authentication production-ready, Simple Chat needs to implement the OAuth 2.0 Client Credentials flow with automatic token refresh: + +#### Required Components: + +1. **Store OAuth Client Credentials** (Not Bearer Token): + ```json + { + "auth_type": "oauth2_client_credentials", + "client_id": "565d53a80dfe4cb89b8869fd1d977308", + "client_secret": "[encrypted_secret]", + "token_endpoint": "https://dev222288.service-now.com/oauth_token.do", + "scope": "useraccount" + } + ``` + +2. **Token Storage with Expiration Tracking**: + ```python + { + "access_token": "EfP7otqXmV...", + "refresh_token": "abc123...", + "expires_at": "2026-01-22T20:17:39Z", # Timestamp + "token_type": "bearer" + } + ``` + +3. **Automatic Token Refresh Logic**: + ```python + def get_valid_token(action_config): + """Get valid token, refreshing if expired""" + if token_expired(action_config): + # Call ServiceNow OAuth token endpoint + response = requests.post( + action_config['token_endpoint'], + data={ + 'grant_type': 'client_credentials', + 'client_id': action_config['client_id'], + 'client_secret': decrypt(action_config['client_secret']) + } + ) + # Update stored token with new access_token and expires_at + update_token_storage(response.json()) + + return get_current_token() + ``` + +4. **Pre-Request Token Validation**: + ```python + # Before each API call in openapi_plugin.py + if auth_config['type'] == 'oauth2_client_credentials': + auth_config['token'] = get_valid_token(auth_config) + headers['Authorization'] = f"Bearer {auth_config['token']}" + ``` + +5. **Secure Secret Storage**: + - Store client secrets in Azure Key Vault (not in Cosmos DB) + - Use Managed Identity for Key Vault access + - Encrypt secrets at rest + +#### Implementation Tasks: + +- [ ] **UI Changes**: Add OAuth 2.0 configuration form (Client ID, Secret, Token Endpoint) +- [ ] **Backend Changes**: + - [ ] Create `oauth2_token_manager.py` module for token lifecycle management + - [ ] Implement token refresh logic with expiration checking + - [ ] Add Key Vault integration for client secret storage + - [ ] Update `openapi_plugin_factory.py` to detect OAuth 2.0 auth type + - [ ] Modify HTTP request preparation to request fresh tokens +- [ ] **Database Schema**: Add token storage fields (access_token, refresh_token, expires_at) +- [ ] **Testing**: End-to-end testing with real OAuth 2.0 endpoints and token expiration scenarios +- [ ] **Documentation**: Update user guide with OAuth 2.0 setup instructions + +#### References: +- [OAuth 2.0 Client Credentials Grant](https://oauth.net/2/grant-types/client-credentials/) +- [ServiceNow OAuth 2.0 Documentation](https://docs.servicenow.com/bundle/washingtondc-platform-security/page/administer/security/concept/c_OAuthApplications.html) +- [Azure Key Vault for Secret Management](https://learn.microsoft.com/azure/key-vault/general/overview) + +**Estimated Effort:** 2-3 weeks for complete implementation and testing + +**Priority:** Medium - Current manual workaround functional for testing/development, critical for production deployment + +--- + +### Monitoring +Track authentication failures by action type to detect similar issues: +```python +# Example monitoring +if response.status_code == 401: + logger.warning(f"Auth failed for {action_type} action: {action_name}") +``` + +## Version History +- **0.235.027** - Group agent loading fix (prerequisite) +- **0.235.028** - Group action schema merging parity fix (this document) diff --git a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md new file mode 100644 index 00000000..62389eb9 --- /dev/null +++ b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md @@ -0,0 +1,241 @@ +# Group Agent Loading Fix + +## Header Information + +**Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode +**Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. +**Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. +**Version Implemented:** 0.235.027 +**Date:** January 22, 2026 + +## Problem Statement + +### Symptoms +When a user selected a group agent in per-user semantic kernel mode: +- The agent selection would fall back to the global "researcher" agent +- Plugin count would be zero (`plugin_count: 0, plugins: []`) +- Agent would ask clarifying questions instead of executing available actions +- No group agents appeared in the available agents list +- Group actions (plugins) were not accessible even though they existed in the database + +### Impact +- **Severity:** High - Group agents completely non-functional in per-user kernel mode +- **Affected Users:** All users with per-user semantic kernel enabled who are members of groups +- **Workaround:** None - only global agents worked + +### Evidence from Logs +``` +[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] Found 2 global agents to merge +[SK Loader] After merging: 3 total agents +[DEBUG] [INFO]: [SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False)] +[DEBUG] [INFO]: [SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False +[DEBUG] [INFO]: [SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent +[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c No agent found matching user-selected agent: cio6_servicenow_test_agent +``` + +Notice: Only 3 agents loaded (2 global + 1 personal), **zero group agents** despite user being member of group "cio6". + +## Root Cause Analysis + +### Architectural Gap +The `load_user_semantic_kernel()` function in `semantic_kernel_loader.py` had the following loading sequence: + +1. ✅ Load personal agents via `get_personal_agents(user_id)` +2. ✅ Conditionally merge global agents if `merge_global_semantic_kernel_with_workspace` enabled +3. ❌ **MISSING:** Load group agents from user's group memberships +4. ✅ Load personal actions via `get_personal_actions(user_id)` +5. ✅ Conditionally merge global actions if merge enabled +6. ❌ **MISSING:** Load group actions from user's group memberships + +### Why It Was Missed +The code had logic to load a **single selected group agent** if explicitly requested, but this was: +- Only triggered when a specific group agent was pre-selected +- Required explicit group ID resolution +- Did not load **all** group agents from user's memberships +- Failed to load group agents proactively for selection + +This created a chicken-and-egg problem: the agent couldn't be selected because it wasn't loaded, and it wasn't loaded unless it was selected. + +## Technical Details + +### Files Modified +1. **`semantic_kernel_loader.py`** (Lines ~1155-1250) + - Added group agent loading after personal agents + - Added group action loading after personal actions + - Removed redundant single-agent loading logic + +2. **`config.py`** (Line 91) + - Updated VERSION from "0.235.026" to "0.235.027" + +### Code Changes + +#### Before (Pseudocode) +```python +agents_cfg = get_personal_agents(user_id) +# Mark personal agents +for agent in agents_cfg: + agent['is_global'] = False + +# Only try to load ONE selected group agent if explicitly requested +if selected_agent_is_group: + # Complex logic to find and add single group agent + +# Merge global agents if enabled +if merge_global: + # Add global agents + +# Load personal actions only +plugin_manifests = get_personal_actions(user_id) +``` + +#### After (Pseudocode) +```python +agents_cfg = get_personal_agents(user_id) +# Mark personal agents +for agent in agents_cfg: + agent['is_global'] = False + agent['is_group'] = False + +# Load ALL group agents from user's group memberships +user_groups = get_user_groups(user_id) +for group in user_groups: + group_agents = get_group_agents(group_id) + for group_agent in group_agents: + # Mark and add to agents_cfg + group_agent['is_global'] = False + group_agent['is_group'] = True + group_agent['group_id'] = group_id + group_agent['group_name'] = group_name + agents_cfg.append(group_agent) + +# Merge global agents if enabled (unchanged) +if merge_global: + # Add global agents + +# Load personal actions +plugin_manifests = get_personal_actions(user_id) + +# Load ALL group actions from user's group memberships +for group in user_groups: + group_actions = get_group_actions(group_id) + plugin_manifests.extend(group_actions) +``` + +### Key Implementation Details + +**Group Agent Loading:** +```python +from functions_group import get_user_groups +from functions_group_agents import get_group_agents + +user_groups = [] # Initialize to empty list +try: + user_groups = get_user_groups(user_id) + print(f"[SK Loader] User '{user_id}' is a member of {len(user_groups)} groups") + + group_agent_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_agents = get_group_agents(group_id) + for group_agent in group_agents: + group_agent['is_global'] = False + group_agent['is_group'] = True + group_agent['group_id'] = group_id + group_agent['group_name'] = group_name + agents_cfg.append(group_agent) + group_agent_count += 1 + print(f"[SK Loader] Loaded {len(group_agents)} agents from group '{group_name}' (id: {group_id})") + + if group_agent_count > 0: + log_event(f"[SK Loader] Loaded {group_agent_count} group agents from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) +except Exception as e: + log_event(f"[SK Loader] Error loading group agents for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) + user_groups = [] # Reset to empty on error +``` + +**Group Action Loading:** +```python +# Load group actions from all groups the user is a member of +try: + group_action_count = 0 + for group in user_groups: + group_id = group.get('id') + group_name = group.get('name', 'Unknown') + if group_id: + group_actions = get_group_actions(group_id, return_type=SecretReturnType.NAME) + plugin_manifests.extend(group_actions) + group_action_count += len(group_actions) + print(f"[SK Loader] Loaded {len(group_actions)} actions from group '{group_name}' (id: {group_id})") + + if group_action_count > 0: + log_event(f"[SK Loader] Loaded {group_action_count} group actions from {len(user_groups)} groups for user '{user_id}'", level=logging.INFO) +except Exception as e: + log_event(f"[SK Loader] Error loading group actions for user '{user_id}': {e}", {"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) +``` + +### Functions Used +- **`get_user_groups(user_id)`** - Returns all groups where user is a member (from `functions_group.py`) +- **`get_group_agents(group_id)`** - Returns all agents for a specific group (from `functions_group_agents.py`) +- **`get_group_actions(group_id, return_type)`** - Returns all actions/plugins for a specific group (from `functions_group_actions.py`) + +### Error Handling +- Both group agent and group action loading are wrapped in try-except blocks +- Errors are logged with full exception tracebacks +- On error, `user_groups` is reset to empty list to prevent downstream issues +- System gracefully degrades to personal + global agents if group loading fails + +## Validation + +### Test Scenario +1. **Setup:** + - User `f016493e-9395-4120-91b5-bac4276b6b6c` is member of group `cio6` (ID: `72254e24-4bc6-4680-bc2e-c56d5214d8e8`) + - Group has agent `cio6_servicenow_test_agent` with action `cio6_servicenow_query_incidents` + - Per-user semantic kernel mode enabled + - Global agent merging enabled + +2. **User Action:** + - User selects group agent `cio6_servicenow_test_agent` + - User submits message: "Show me all ServiceNow incidents" + +### Before Fix - Failure Behavior +``` +[SK Loader] User settings found 1 agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] After merging: 3 total agents # Only personal + global +[SK Loader] Looking for agent named 'cio6_servicenow_test_agent' with is_global=False +[SK Loader] User NO agent found matching user-selected agent: cio6_servicenow_test_agent +[SK Loader] selected_agent fallback to first agent: researcher # ❌ Wrong agent +[Enhanced Agent Citations] Extracted 0 detailed plugin invocations # ❌ No actions +{'agent': 'researcher', 'plugin_count': 0} # ❌ Zero plugins +``` + +**Result:** Agent asks clarifying questions instead of querying ServiceNow. + +### After Fix - Success Behavior +``` +[SK Loader] User settings found 1 personal agents for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] User 'f016493e-9395-4120-91b5-bac4276b6b6c' is a member of 1 groups # ✅ Groups detected +[SK Loader] Loaded 1 agents from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group agent loaded +[SK Loader] Loaded 1 group agents from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success +[SK Loader] Total agents loaded: 2 (personal + group) for user 'f016493e-9395-4120-91b5-bac4276b6b6c' +[SK Loader] After merging: 4 total agents # ✅ Includes group agent +[SK Loader] Merged agents: [('researcher', True), ('servicenow_test_agent', True), ('researcher', False), ('cio6_servicenow_test_agent', False)] # ✅ Group agent present +[SK Loader] Loaded 1 actions from group 'cio6' (id: 72254e24-4bc6-4680-bc2e-c56d5214d8e8) # ✅ Group action loaded +[SK Loader] Loaded 1 group actions from 1 groups for user 'f016493e-9395-4120-91b5-bac4276b6b6c' # ✅ Success +[SK Loader] User f016493e-9395-4120-91b5-bac4276b6b6c Found EXACT match for agent: cio6_servicenow_test_agent (is_global=False) # ✅ Agent found +[SK Loader] Plugin cio6_servicenow_query_incidents: SUCCESS # ✅ Plugin loaded +``` + +**Result:** Correct group agent selected with its action available for execution. + +### Verification Checklist +- [x] Personal agents still load correctly +- [x] Global agents still merge correctly when enabled +- [x] Group agents load for all user's group memberships +- [x] Group actions load for all user's group memberships +- [x] Agents properly marked with `is_group` and `group_id` flags +- [x] Agent selection finds group agents by name +- [x] Error handling prevents crashes if group loading fails +- [x] Logging provides visibility into group loading process \ No newline at end of file diff --git a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md new file mode 100644 index 00000000..34eadb4a --- /dev/null +++ b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md @@ -0,0 +1,205 @@ +# OpenAPI Basic Authentication Fix + +**Version:** 0.235.026 +**Issue:** OpenAPI actions with Basic Authentication fail with "session not authenticated" error +**Root Cause:** Mismatch between authentication format stored by UI and format expected by OpenAPI plugin +**Status:** ✅ Fixed + +--- + +## Problem Description + +When configuring an OpenAPI action with Basic Authentication in the Simple Chat admin interface: + +1. User uploads OpenAPI spec with `securitySchemes.basicAuth` defined +2. User selects "Basic Auth" authentication type +3. User enters username and password in the configuration wizard +4. Action is saved successfully +5. **BUT**: When agent attempts to use the action, authentication fails with error: + ``` + "I'm unable to access your ServiceNow incidents because your session + is not authenticated. Please log in to your ServiceNow instance or + check your authentication credentials." + ``` + +### Symptoms +- ❌ OpenAPI actions with Basic Auth fail despite correct credentials +- ✅ Direct API calls with same credentials work correctly +- ✅ Other Simple Chat features authenticate successfully +- ❌ Error occurs even when Base URL is correctly configured + +--- + +## Root Cause Analysis + +### Authentication Storage Format (Frontend) + +The Simple Chat admin UI (`plugin_modal_stepper.js`, lines 1539-1543) stores Basic Auth credentials as: + +```javascript +auth.type = 'key'; // Basic auth is also 'key' type in the schema +const username = document.getElementById('plugin-auth-basic-username').value.trim(); +const password = document.getElementById('plugin-auth-basic-password').value.trim(); +auth.key = `${username}:${password}`; // Store as combined string +additionalFields.auth_method = 'basic'; +``` + +**Stored format:** +```json +{ + "auth": { + "type": "key", + "key": "username:password" + }, + "additionalFields": { + "auth_method": "basic" + } +} +``` + +### Authentication Expected Format (Backend) + +The OpenAPI plugin (`openapi_plugin.py`, lines 952-955) expects Basic Auth as: + +```python +elif auth_type == "basic": + import base64 + username = self.auth.get("username", "") + password = self.auth.get("password", "") + credentials = base64.b64encode(f"{username}:{password}".encode()).decode() + headers["Authorization"] = f"Basic {credentials}" +``` + +**Expected format:** +```json +{ + "auth": { + "type": "basic", + "username": "actual_username", + "password": "actual_password" + } +} +``` + +### The Mismatch + +❌ **Frontend stores:** `auth.type='key'`, `auth.key='username:password'` +❌ **Backend expects:** `auth.type='basic'`, `auth.username`, `auth.password` +❌ **Result:** Plugin code path for Basic Auth (`elif auth_type == "basic"`) never executes +❌ **Consequence:** No `Authorization` header added, API returns authentication error + +--- + +## Solution Implementation + +### Fix Location +**File:** `application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py` +**Function:** `_extract_auth_config()` +**Lines:** 129-166 + +### Code Changes + +Added authentication format transformation logic to detect and convert Simple Chat's storage format into OpenAPI plugin's expected format: + +```python +@classmethod +def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: + """Extract authentication configuration from plugin config.""" + auth_config = config.get('auth', {}) + if not auth_config: + return {} + + auth_type = auth_config.get('type', 'none') + + if auth_type == 'none': + return {} + + # Check if this is basic auth stored in the 'key' field format + # Simple Chat stores basic auth as: auth.type='key', auth.key='username:password', + # additionalFields.auth_method='basic' + additional_fields = config.get('additionalFields', {}) + auth_method = additional_fields.get('auth_method', '') + + if auth_type == 'key' and auth_method == 'basic': + # Extract username and password from the combined key + key = auth_config.get('key', '') + if ':' in key: + username, password = key.split(':', 1) + return { + 'type': 'basic', + 'username': username, + 'password': password + } + else: + # Malformed basic auth key + return {} + + # For bearer tokens stored as 'key' type + if auth_type == 'key' and auth_method == 'bearer': + return { + 'type': 'bearer', + 'token': auth_config.get('key', '') + } + + # For OAuth2 stored as 'key' type + if auth_type == 'key' and auth_method == 'oauth2': + return { + 'type': 'bearer', # OAuth2 tokens are typically bearer tokens + 'token': auth_config.get('key', '') + } + + # Return the auth config as-is for other auth types + return auth_config +``` + +### How It Works + +1. **Detection:** Check if `auth.type == 'key'` AND `additionalFields.auth_method == 'basic'` +2. **Extraction:** Split `auth.key` on first `:` to get username and password +3. **Transformation:** Return new dict with `type='basic'`, `username`, and `password` +4. **Pass-through:** OpenAPI plugin receives correct format and adds Authorization header + +### Additional Auth Method Support + +The fix also handles other authentication methods stored in the same format: +- **Bearer tokens:** `auth_method='bearer'` → transforms to `{type: 'bearer', token: ...}` +- **OAuth2:** `auth_method='oauth2'` → transforms to `{type: 'bearer', token: ...}` + +--- + +## Testing + +### Before Fix +```bash +# Test action: servicenow_query_incidents +User: "Show me all incidents in ServiceNow" +Agent: "I'm unable to access your ServiceNow incidents because your + session is not authenticated..." + +# HTTP request (no Authorization header sent): +GET https://dev222288.service-now.com/api/now/table/incident +# Response: 401 Unauthorized or session expired error +``` + +### After Fix +```bash +# Test action: servicenow_query_incidents +User: "Show me all incidents in ServiceNow" +Agent: "Here are your ServiceNow incidents: ..." + +# HTTP request (Authorization header correctly added): +GET https://dev222288.service-now.com/api/now/table/incident +Authorization: Basic + +# Response: 200 OK with incident data +``` + +### Validation Steps +1. ✅ Create OpenAPI action with Basic Auth +2. ✅ Enter username and password in admin wizard +3. ✅ Save action successfully +4. ✅ Attach action to agent +5. ✅ Test agent with prompt requiring action +6. ✅ Verify Authorization header is sent +7. ✅ Verify API returns 200 OK with data +8. ✅ Verify agent processes response correctly \ No newline at end of file From dce54a19acce8a53d135e0e13451d61747ae922b Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:17:59 -0500 Subject: [PATCH 13/35] Added version number to the feature readme files --- docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md | 3 +++ docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md index 4d400961..00e73fc5 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -1,5 +1,8 @@ # ServiceNow Integration Guide +**Version:** 0.236.012 +**Implemented in version:** 0.236.012 + ## Overview This guide documents the integration between Simple Chat and ServiceNow, enabling AI-powered incident management, ticket analysis, and support operations through natural language prompts. diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md index 02646450..19a7407d 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md @@ -1,5 +1,8 @@ # ServiceNow OAuth 2.0 Setup for Simple Chat +**Version:** 0.236.012 +**Implemented in version:** 0.236.012 + ## Overview This guide shows you how to configure OAuth 2.0 bearer token authentication for ServiceNow integration with Simple Chat using the **modern "New Inbound Integration Experience"** method. This is more secure than Basic Auth and recommended for production environments. From 8353d771b34c14aa97fc6b8f54c3557d9bc7bf29 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:29:02 -0500 Subject: [PATCH 14/35] Added version number to document, and removed redudant import statement --- application/single_app/semantic_kernel_loader.py | 5 +---- docs/how-to/azure_speech_managed_identity_manul_setup.md | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/application/single_app/semantic_kernel_loader.py b/application/single_app/semantic_kernel_loader.py index 9248ad6b..1e0e620b 100644 --- a/application/single_app/semantic_kernel_loader.py +++ b/application/single_app/semantic_kernel_loader.py @@ -34,7 +34,7 @@ from functions_global_agents import get_global_agents from functions_group_agents import get_group_agent, get_group_agents from functions_group_actions import get_group_actions -from functions_group import require_active_group +from functions_group import require_active_group, get_user_groups from functions_personal_actions import get_personal_actions, ensure_migration_complete as ensure_actions_migration_complete from functions_personal_agents import get_personal_agents, ensure_migration_complete as ensure_agents_migration_complete from semantic_kernel_plugins.plugin_loader import discover_plugins @@ -1188,9 +1188,6 @@ def load_user_semantic_kernel(kernel: Kernel, settings, user_id: str, redis_clie agent['is_group'] = False # Load group agents from all groups the user is a member of - from functions_group import get_user_groups - from functions_group_agents import get_group_agents - user_groups = [] # Initialize to empty list try: user_groups = get_user_groups(user_id) diff --git a/docs/how-to/azure_speech_managed_identity_manul_setup.md b/docs/how-to/azure_speech_managed_identity_manul_setup.md index bf1b6e74..1db0ceca 100644 --- a/docs/how-to/azure_speech_managed_identity_manul_setup.md +++ b/docs/how-to/azure_speech_managed_identity_manul_setup.md @@ -1,5 +1,7 @@ # Azure Speech Service with Managed Identity Manual Setup +Version: 0.236.012 + ## Overview This guide explains the critical difference between key-based and managed identity authentication when configuring Azure Speech Service, and the required steps to enable managed identity properly. From 5aa7007d251aa4a4f0fc149e93d511955a6b1029 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:42:07 -0500 Subject: [PATCH 15/35] refactor: use _ for intentionally unused variable in AI Search test - Changed 'indexes = list(...)' to '_ = list(...)' - Follows Python convention for discarded return values - AI Search connection test only needs to verify the API call succeeds --- application/single_app/route_backend_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index 9df4b3ee..30e10cb2 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -795,7 +795,7 @@ def _test_azure_ai_search_connection(payload): client = SearchIndexClient(endpoint=endpoint, credential=credential) # Test by listing indexes (simple operation to verify connectivity) - indexes = list(client.list_indexes()) + _ = list(client.list_indexes()) return jsonify({'message': 'Azure AI search connection successful'}), 200 except Exception as e: From 548d8d8d7d1f0d4467682b25273e25b60653433f Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:45:25 -0500 Subject: [PATCH 16/35] Removed azure_speech_managed_indeity_manual readme file since it is unrelated to this servicenow integration --- ...ure_speech_managed_identity_manul_setup.md | 263 ------------------ 1 file changed, 263 deletions(-) delete mode 100644 docs/how-to/azure_speech_managed_identity_manul_setup.md diff --git a/docs/how-to/azure_speech_managed_identity_manul_setup.md b/docs/how-to/azure_speech_managed_identity_manul_setup.md deleted file mode 100644 index 1db0ceca..00000000 --- a/docs/how-to/azure_speech_managed_identity_manul_setup.md +++ /dev/null @@ -1,263 +0,0 @@ -# Azure Speech Service with Managed Identity Manual Setup - -Version: 0.236.012 - -## Overview - -This guide explains the critical difference between key-based and managed identity authentication when configuring Azure Speech Service, and the required steps to enable managed identity properly. - -## Authentication Methods: Regional vs. Resource-Specific Endpoints - -### Regional Endpoint (Shared Gateway) - -**Endpoint format**: `https://.api.cognitive.microsoft.com` -- Example: `https://eastus2.api.cognitive.microsoft.com` -- This is a **shared endpoint** for all Speech resources in that Azure region -- Acts as a gateway that routes requests to individual Speech resources - -### Resource-Specific Endpoint (Custom Subdomain) - -**Endpoint format**: `https://.cognitiveservices.azure.com` -- Example: `https://simplechat6-dev-speech.cognitiveservices.azure.com` -- This is a **unique endpoint** dedicated to your specific Speech resource -- Requires custom subdomain to be enabled on the resource - ---- - -## Why Regional Endpoint Works with Key but NOT Managed Identity - -### Key-Based Authentication ✅ Works with Regional Endpoint - -When using subscription key authentication: - -```http -POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe -Headers: - Ocp-Apim-Subscription-Key: abc123def456... -``` - -**Why it works:** -1. The subscription key **directly identifies** your specific Speech resource -2. The regional gateway uses the key to look up which resource it belongs to -3. The request is automatically routed to your resource -4. Authorization succeeds because the key proves ownership - -### Managed Identity (AAD Token) ❌ Fails with Regional Endpoint - -When using managed identity authentication: - -```http -POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe -Headers: - Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... -``` - -**Why it fails (returns 400 BadRequest):** -1. The Bearer token proves your App Service identity to Azure AD -2. The token does NOT specify which Speech resource you want to access -3. The regional gateway cannot determine: - - Which specific Speech resource you're authorized for - - Whether your managed identity has RBAC roles on that resource -4. **Result**: The gateway rejects the request with 400 BadRequest - -### Managed Identity ✅ Works with Resource-Specific Endpoint - -When using managed identity with custom subdomain: - -```http -POST https://simplechat6-dev-speech.cognitiveservices.azure.com/speechtotext/transcriptions:transcribe -Headers: - Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... -``` - -**Why it works:** -1. The hostname **itself identifies** your specific Speech resource -2. Azure validates your managed identity Bearer token against that resource's RBAC -3. If your App Service MI has `Cognitive Services Speech User` role → authorized -4. The request proceeds to your dedicated Speech resource instance - ---- - -## Required Setup for Managed Identity - -### Prerequisites - -1. **Azure Speech Service resource** created in your subscription -2. **System-assigned or user-assigned managed identity** on your App Service -3. **RBAC role assignments** on the Speech resource - -### Step 1: Enable Custom Subdomain on Speech Resource - -**Why needed**: By default, Speech resources use the regional endpoint and do NOT have custom subdomains. Managed identity requires the resource-specific endpoint. - -**How to enable**: - -```bash -az cognitiveservices account update \ - --name \ - --resource-group \ - --custom-domain -``` - -**Example**: - -```bash -az cognitiveservices account update \ - --name simplechat6-dev-speech \ - --resource-group sc-simplechat6-dev-rg \ - --custom-domain simplechat6-dev-speech -``` - -**Important notes**: -- Custom subdomain name must be **globally unique** across Azure -- Usually use the same name as your resource: `` -- **One-way operation**: Cannot be disabled once enabled -- After enabling, the resource's endpoint property changes from regional to resource-specific - -**Verify custom subdomain is enabled**: - -```bash -az cognitiveservices account show \ - --name \ - --resource-group \ - --query "{customSubDomainName:properties.customSubDomainName, endpoint:properties.endpoint}" -``` - -Expected output: -```json -{ - "customSubDomainName": "simplechat6-dev-speech", - "endpoint": "https://simplechat6-dev-speech.cognitiveservices.azure.com/" -} -``` - -### Step 2: Assign RBAC Roles to Managed Identity - -Grant your App Service managed identity the necessary roles on the Speech resource: - -```bash -# Get the Speech resource ID -SPEECH_RESOURCE_ID=$(az cognitiveservices account show \ - --name \ - --resource-group \ - --query id -o tsv) - -# Get the App Service managed identity principal ID -MI_PRINCIPAL_ID=$(az webapp identity show \ - --name \ - --resource-group \ - --query principalId -o tsv) - -# Assign Cognitive Services Speech User role (data-plane read access) -az role assignment create \ - --assignee $MI_PRINCIPAL_ID \ - --role "Cognitive Services Speech User" \ - --scope $SPEECH_RESOURCE_ID - -# Assign Cognitive Services Speech Contributor role (if needed for write operations) -az role assignment create \ - --assignee $MI_PRINCIPAL_ID \ - --role "Cognitive Services Speech Contributor" \ - --scope $SPEECH_RESOURCE_ID -``` - -**Verify role assignments**: - -```bash -az role assignment list \ - --assignee $MI_PRINCIPAL_ID \ - --scope $SPEECH_RESOURCE_ID \ - -o table -``` - -### Step 3: Configure Admin Settings - -In the Admin Settings → Search & Extract → Multimedia Support section: - -| Setting | Value | Example | -|---------|-------|---------| -| **Enable Audio File Support** | ✅ Checked | | -| **Speech Service Endpoint** | Resource-specific endpoint (with custom subdomain) | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | -| **Speech Service Location** | Azure region | `eastus2` | -| **Speech Service Locale** | Language locale for transcription | `en-US` | -| **Authentication Type** | Managed Identity | | -| **Speech Service Key** | (Leave empty when using MI) | | - -**Critical**: -- Endpoint must be the resource-specific URL (custom subdomain) -- Do NOT use the regional endpoint for managed identity -- Remove trailing slash from endpoint: ✅ `https://..azure.com` ❌ `https://..azure.com/` - -### Step 4: Test Audio Upload - -1. Upload a short WAV or MP3 file -2. Monitor application logs for transcription progress -3. Expected log output: - ``` - File size: 1677804 bytes - Produced 1 WAV chunks: ['/tmp/tmp_chunk_000.wav'] - [Debug] Transcribing WAV chunk: /tmp/tmp_chunk_000.wav - [Debug] Speech config obtained successfully - [Debug] Received 5 phrases - Creating 3 transcript pages - ``` - ---- - -## Troubleshooting - -### Error: NameResolutionError - Failed to resolve hostname - -**Symptom**: `Failed to resolve 'simplechat6-dev-speech.cognitiveservices.azure.com'` - -**Cause**: Custom subdomain not enabled on Speech resource - -**Solution**: Enable custom subdomain using Step 1 above - -### Error: 400 BadRequest when using MI with regional endpoint - -**Symptom**: `400 Client Error: BadRequest for url: https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe` - -**Cause**: Managed identity requires resource-specific endpoint, not regional - -**Solution**: Update Admin Settings endpoint to use `https://.cognitiveservices.azure.com` - -### Error: 401 Authentication error with MI - -**Symptom**: `WebSocket upgrade failed: Authentication error (401)` - -**Cause**: Missing RBAC role assignments - -**Solution**: Assign required roles using Step 2 above - -### Key auth works but MI fails - -**Diagnosis checklist**: -- [ ] Custom subdomain enabled on Speech resource? -- [ ] Admin Settings endpoint is resource-specific (not regional)? -- [ ] Managed identity has RBAC roles on Speech resource? -- [ ] Authentication Type set to "Managed Identity" in Admin Settings? - ---- - -## Summary - -| Authentication Method | Endpoint Type | Example | Works? | -|----------------------|---------------|---------|--------| -| **Key** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ✅ Yes | -| **Key** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes | -| **Managed Identity** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ❌ No (400 BadRequest) | -| **Managed Identity** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes (with custom subdomain) | - -**Key takeaway**: Managed identity for Azure Cognitive Services data-plane operations requires: -1. Custom subdomain enabled on the resource -2. Resource-specific endpoint configured in your application -3. RBAC roles assigned to the managed identity at the resource scope - ---- - -## References - -- [Azure Cognitive Services custom subdomain documentation](https://learn.microsoft.com/azure/cognitive-services/cognitive-services-custom-subdomains) -- [Authenticate with Azure AD using managed identity](https://learn.microsoft.com/azure/cognitive-services/authentication?tabs=powershell#authenticate-with-azure-active-directory) -- [Azure Speech Service authentication](https://learn.microsoft.com/azure/ai-services/speech-service/rest-speech-to-text-short) From 62b0b5b1caa9d9a0806bc07523e16c919813bdd3 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:52:21 -0500 Subject: [PATCH 17/35] update version numbers to 0.236.012 in bug fix documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OPENAPI_BASIC_AUTH_FIX.md: 0.235.026 → 0.236.012 - GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md: 0.235.028 → 0.236.012 - GROUP_AGENT_LOADING_FIX.md: 0.235.027 → 0.236.012 - AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md: 0.235.004 → 0.236.012" --- .../fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 2 +- .../fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 2 +- docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md | 2 +- docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md index 0d507ecc..a4b6a862 100644 --- a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md +++ b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md @@ -213,7 +213,7 @@ The SDK approach eliminates the need for the `search_resource_manager` variable ## Version Information -**Version Implemented:** 0.235.004 +**Version Implemented:** 0.236.012 ## References diff --git a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md index 196bc132..bcfac1ff 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -5,7 +5,7 @@ **Fix Title:** Group Actions Missing `additionalFields` Causing OAuth Authentication Failures **Issue Description:** Group actions were missing the `additionalFields` property entirely, preventing OAuth bearer token authentication from working despite having the same configuration as working global actions. **Root Cause:** Group action backend routes did not call `get_merged_plugin_settings()` to merge UI form data with schema defaults, while global action routes did. This caused group actions to be saved without authentication configuration fields. -**Version Implemented:** 0.235.028 +**Version Implemented:** 0.236.012 **Date:** January 22, 2026 ## Problem Statement diff --git a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md index 62389eb9..a8e18e6b 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md @@ -5,7 +5,7 @@ **Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode **Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. **Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. -**Version Implemented:** 0.235.027 +**Version Implemented:** 0.236.012 **Date:** January 22, 2026 ## Problem Statement diff --git a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md index 34eadb4a..30f857b1 100644 --- a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md +++ b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md @@ -1,6 +1,7 @@ # OpenAPI Basic Authentication Fix -**Version:** 0.235.026 +**Version:** 0.236.012 + **Issue:** OpenAPI actions with Basic Authentication fail with "session not authenticated" error **Root Cause:** Mismatch between authentication format stored by UI and format expected by OpenAPI plugin **Status:** ✅ Fixed From b0be501e1f6306d9822c16e49304791317d55bdb Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:53:38 -0500 Subject: [PATCH 18/35] Update application/single_app/semantic_kernel_loader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- application/single_app/semantic_kernel_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/single_app/semantic_kernel_loader.py b/application/single_app/semantic_kernel_loader.py index 1e0e620b..8c3919ac 100644 --- a/application/single_app/semantic_kernel_loader.py +++ b/application/single_app/semantic_kernel_loader.py @@ -34,7 +34,7 @@ from functions_global_agents import get_global_agents from functions_group_agents import get_group_agent, get_group_agents from functions_group_actions import get_group_actions -from functions_group import require_active_group, get_user_groups +from functions_group import get_user_groups from functions_personal_actions import get_personal_actions, ensure_migration_complete as ensure_actions_migration_complete from functions_personal_agents import get_personal_agents, ensure_migration_complete as ensure_agents_migration_complete from semantic_kernel_plugins.plugin_loader import discover_plugins From e264a1342bf9ebff0724a24b06c9efb928d75447 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:55:01 -0500 Subject: [PATCH 19/35] Update docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../open_api_specs/sample_now_knowledge_latest_spec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml index 7b57cb85..9cd76173 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml @@ -8,8 +8,8 @@ info: url: https://developer.servicenow.com servers: - - url: https://dev222288.service-now.com - description: ServiceNow Developer Instance + - url: https://YOUR-INSTANCE.service-now.com + description: ServiceNow instance (replace YOUR-INSTANCE with your instance name) security: - bearerAuth: [] From b04bd671ccfcb1d6510db49b1719d9a552734f2b Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:55:45 -0500 Subject: [PATCH 20/35] Update docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md index a4b6a862..ea981e7a 100644 --- a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md +++ b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md @@ -213,7 +213,8 @@ The SDK approach eliminates the need for the `search_resource_manager` variable ## Version Information -**Version Implemented:** 0.236.012 +- Application version (`config.py` `app.config['VERSION']`): **0.236.012** +- Fixed in version: **0.236.012** ## References From 84c01e98359ad4464f765210580405c785c2f2d2 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:56:25 -0500 Subject: [PATCH 21/35] Update docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../sample_now_knowledge_latest_spec_basicauth.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml index e4262b87..5a10a1fe 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml @@ -8,8 +8,8 @@ info: url: https://developer.servicenow.com servers: - - url: https://dev222288.service-now.com - description: ServiceNow Developer Instance + - url: https://YOUR-INSTANCE.service-now.com + description: Example ServiceNow instance URL - replace YOUR-INSTANCE with your own instance name security: - basicAuth: [] From c8e383ed2242428eb8812e428cbb4a7e1fb29691 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:56:35 -0500 Subject: [PATCH 22/35] Update docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../sample_servicenow_incident_api_basicauth.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml index dc4e1b3a..2ce9e256 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow_incident_api_basicauth.yaml @@ -8,8 +8,8 @@ info: url: https://developer.servicenow.com servers: - - url: https://dev222288.service-now.com/api/now - description: ServiceNow Developer Instance + - url: https://YOUR-INSTANCE.service-now.com/api/now + description: ServiceNow instance base URL (replace YOUR-INSTANCE with your own instance name) security: - basicAuth: [] From 39dc7a40a9d58a8f0afdd5bde089bd1f04bacdd4 Mon Sep 17 00:00:00 2001 From: vivche Date: Fri, 23 Jan 2026 20:57:24 -0500 Subject: [PATCH 23/35] Update docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md index a8e18e6b..cb5045e4 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md @@ -5,7 +5,7 @@ **Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode **Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. **Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. -**Version Implemented:** 0.236.012 +**Fixed/Implemented in version:** **0.236.012** (matches `config.py` `app.config['VERSION']`) **Date:** January 22, 2026 ## Problem Statement From 2e8c73728cab7be899ed747a2bf8a1af86c26ba8 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Fri, 23 Jan 2026 20:59:36 -0500 Subject: [PATCH 24/35] Remvoed debug statements that might include senstive info --- .../semantic_kernel_plugins/openapi_plugin_factory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py index 3194b7b7..f11f3d66 100644 --- a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py +++ b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py @@ -128,7 +128,6 @@ def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: from functions_debug import debug_print auth_config = config.get('auth', {}) - debug_print(f"[Factory] Initial auth_config: {auth_config}") if not auth_config: return {} @@ -176,6 +175,5 @@ def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: 'token': auth_config.get('key', '') } - debug_print(f"[Factory] Returning auth as-is: {auth_config}") # Return the auth config as-is for other auth types return auth_config From 0c23a78cc7a63445b16725df2a7e771c9bb18193 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Sat, 24 Jan 2026 09:18:24 -0500 Subject: [PATCH 25/35] Rollback Azure AI Search test connection fix for separate PR Reverted route_backend_settings.py to origin/development version and removed AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md documentation. These changes will be submitted in a dedicated PR to keep the ServiceNow integration PR focused. --- .../single_app/route_backend_settings.py | 71 +++--- .../AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md | 227 ------------------ 2 files changed, 34 insertions(+), 264 deletions(-) delete mode 100644 docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index 30e10cb2..be182e93 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -761,45 +761,42 @@ def _test_azure_ai_search_connection(payload): """Attempt to connect to Azure Cognitive Search (or APIM-wrapped).""" enable_apim = payload.get('enable_apim', False) - try: - if enable_apim: - apim_data = payload.get('apim', {}) - endpoint = apim_data.get('endpoint') - subscription_key = apim_data.get('subscription_key') - - # Use SearchIndexClient for APIM - credential = AzureKeyCredential(subscription_key) - client = SearchIndexClient(endpoint=endpoint, credential=credential) - else: - direct_data = payload.get('direct', {}) - endpoint = direct_data.get('endpoint') - key = direct_data.get('key') - - if direct_data.get('auth_type') == 'managed_identity': - credential = DefaultAzureCredential() - # For managed identity, use the SDK which handles authentication properly - if AZURE_ENVIRONMENT in ("usgovernment", "custom"): - client = SearchIndexClient( - endpoint=endpoint, - credential=credential, - audience=search_resource_manager - ) - else: - # For public cloud, don't use audience parameter - client = SearchIndexClient( - endpoint=endpoint, - credential=credential - ) - else: - credential = AzureKeyCredential(key) - client = SearchIndexClient(endpoint=endpoint, credential=credential) + if enable_apim: + apim_data = payload.get('apim', {}) + endpoint = apim_data.get('endpoint') # e.g. https://my-apim.azure-api.net/search + subscription_key = apim_data.get('subscription_key') + url = f"{endpoint.rstrip('/')}/indexes?api-version=2023-11-01" + headers = { + 'api-key': subscription_key, + 'Content-Type': 'application/json' + } + else: + direct_data = payload.get('direct', {}) + endpoint = direct_data.get('endpoint') # e.g. https://.search.windows.net + key = direct_data.get('key') + url = f"{endpoint.rstrip('/')}/indexes?api-version=2023-11-01" - # Test by listing indexes (simple operation to verify connectivity) - _ = list(client.list_indexes()) + if direct_data.get('auth_type') == 'managed_identity': + credential_scopes=search_resource_manager + "/.default" + arm_scope = credential_scopes + credential = DefaultAzureCredential() + arm_token = credential.get_token(arm_scope).token + headers = { + 'Authorization': f'Bearer {arm_token}', + 'Content-Type': 'application/json' + } + else: + headers = { + 'api-key': key, + 'Content-Type': 'application/json' + } + + # A small GET to /indexes to verify we have connectivity + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code == 200: return jsonify({'message': 'Azure AI search connection successful'}), 200 - - except Exception as e: - return jsonify({'error': f'Azure AI search connection error: {str(e)}'}), 500 + else: + raise Exception(f"Azure AI search connection error: {resp.status_code} - {resp.text}") def _test_azure_doc_intelligence_connection(payload): diff --git a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md b/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md deleted file mode 100644 index ea981e7a..00000000 --- a/docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECTION_FIX.md +++ /dev/null @@ -1,227 +0,0 @@ -# Azure AI Search Test Connection Fix - -## Issue Description - -When clicking the "Test Azure AI Search Connection" button on the App Settings "Search & Extract" page with **managed identity authentication** enabled, the test connection failed with the following error: - -**Original Error Message:** -``` -NameError: name 'search_resource_manager' is not defined -``` - -**Environment Configuration:** -- Authentication Type: Managed Identity -- Azure Environment: `public` (set in .env file) -- Error occurred because the code tried to reference `search_resource_manager` which wasn't defined for public cloud - -**Root Cause:** The old implementation used a REST API approach that required the `search_resource_manager` variable to construct authentication tokens. This variable wasn't defined for the public cloud environment, causing the initial error. Even if defined, the REST API approach with bearer tokens doesn't work properly with Azure AI Search's managed identity authentication. - -## Root Cause Analysis - -The old implementation used a **REST API approach with manually acquired bearer tokens**, which is fundamentally incompatible with how Azure AI Search handles managed identity authentication on the data plane. - -### Why the Old Approach Failed - -Azure AI Search's data plane operations don't properly accept bearer tokens acquired through standard `DefaultAzureCredential.get_token()` flows and passed as HTTP Authorization headers. The authentication mechanism works differently: - -```python -# OLD IMPLEMENTATION - FAILED ❌ -credential = DefaultAzureCredential() -arm_scope = f"{search_resource_manager}/.default" -token = credential.get_token(arm_scope).token - -headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json" -} -response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) -# Returns: 403 Forbidden -``` - -**Problems with this approach:** -1. Azure AI Search requires SDK-specific authentication handling -2. Bearer tokens from `get_token()` are rejected by the Search service -3. Token scope and refresh logic need specialized handling -4. This issue occurs in **all Azure environments** (public, government, custom) - -### Why Other Services Work with REST API + Bearer Tokens - -Some Azure services accept bearer tokens in REST API calls, but Azure AI Search requires the SDK to: -1. Acquire tokens using the correct scope and flow -2. Handle token refresh automatically -3. Use Search-specific authentication headers -4. Properly negotiate with the Search service's auth layer - -## Technical Details - -### Files Modified - -**File:** `route_backend_settings.py` -**Function:** `_test_azure_ai_search_connection(payload)` -**Lines:** 760-796 - -### The Solution - -Instead of trying to define `search_resource_manager` for public cloud, the fix was to **replace the REST API approach entirely with the SearchIndexClient SDK**, which handles authentication correctly without needing the `search_resource_manager` variable. - -### Code Changes Summary - -**Before (REST API approach):** -```python -def _test_azure_ai_search_connection(payload): - # ... setup code ... - - if direct_data.get('auth_type') == 'managed_identity': - credential = DefaultAzureCredential() - arm_scope = f"{search_resource_manager}/.default" - token = credential.get_token(arm_scope).token - - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json" - } - response = requests.get(f"{endpoint}/indexes?api-version=2024-07-01", headers=headers) - # ❌ Returns 403 Forbidden -``` - -**After (SDK approach):** -```python -def _test_azure_ai_search_connection(payload): - # ... setup code ... - - if direct_data.get('auth_type') == 'managed_identity': - credential = DefaultAzureCredential() - - # Use SDK which handles authentication properly - if AZURE_ENVIRONMENT in ("usgovernment", "custom"): - client = SearchIndexClient( - endpoint=endpoint, - credential=credential, - audience=search_resource_manager - ) - else: - # For public cloud, don't use audience parameter - client = SearchIndexClient( - endpoint=endpoint, - credential=credential - ) - - # Test by listing indexes (simple operation to verify connectivity) - indexes = list(client.list_indexes()) - # ✅ Works correctly -``` - -### Key Implementation Details - -1. **Replaced REST API with SearchIndexClient SDK** - - Uses `SearchIndexClient` from `azure.search.documents` - - SDK handles authentication internally - - Properly manages token acquisition and refresh - -2. **Environment-Specific Configuration** - - **Azure Government/Custom:** Requires `audience` parameter - - **Azure Public Cloud:** Omits `audience` parameter - - Matches pattern used throughout codebase - -3. **Consistent with Other Functions** - - Aligns with `get_index_client()` implementation (line 484) - - Matches SearchClient initialization in `config.py` (lines 584-619) - - All other search operations already use SDK approach - -## Testing Approach - -### Prerequisites -- Service principal must have **"Search Index Data Contributor"** RBAC role -- Permissions must propagate (5-10 minutes after assignment) - -### RBAC Role Assignment Command -```bash -az role assignment create \ - --assignee \ - --role "Search Index Data Contributor" \ - --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ -``` - -### Verification -```bash -az role assignment list \ - --assignee \ - --scope /subscriptions//resourceGroups//providers/Microsoft.Search/searchServices/ \ - --output table -``` - -## Impact Analysis - -### What Changed -- **Only the test connection function** was affected -- No changes needed to actual search operations (indexing, querying, etc.) -- All other search functionality already used correct SDK approach - -### Why Other Search Operations Weren't Affected -All production search operations throughout the codebase already use the SDK: -- `SearchClient` for querying indexes -- `SearchIndexClient` for managing indexes -- `get_index_client()` helper function -- Index initialization in `config.py` - -**Only the test connection function used the failed REST API approach.** - -## Validation - -### Before Fix -- ✅ Authentication succeeded (no credential errors) -- ✅ Token acquisition worked -- ❌ Azure AI Search rejected bearer token (403 Forbidden) -- ❌ Test connection failed - -### After Fix -- ✅ Authentication succeeds -- ✅ SDK handles token acquisition properly -- ✅ Azure AI Search accepts SDK authentication -- ✅ Test connection succeeds (with proper RBAC permissions) - -## Configuration Requirements - -### Public Cloud (.env) -```ini -AZURE_ENVIRONMENT=public -AZURE_CLIENT_ID= -AZURE_CLIENT_SECRET= -AZURE_TENANT_ID= -AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity -AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.net -``` - -### Azure Government (.env) -```ini -AZURE_ENVIRONMENT=usgovernment -AZURE_CLIENT_ID= -AZURE_CLIENT_SECRET= -AZURE_TENANT_ID= -AZURE_AI_SEARCH_AUTHENTICATION_TYPE=managed_identity -AZURE_AI_SEARCH_ENDPOINT=https://.search.windows.us -``` - -## Related Changes - -**No config.py changes were made.** The fix was entirely in the route_backend_settings.py file by switching from REST API to SDK approach. - -The SDK approach eliminates the need for the `search_resource_manager` variable in public cloud because: -- The SearchIndexClient handles authentication internally -- No manual token acquisition is needed -- The SDK knows the correct endpoints and scopes automatically - -## Version Information - -- Application version (`config.py` `app.config['VERSION']`): **0.236.012** -- Fixed in version: **0.236.012** - -## References - -- Azure AI Search SDK Documentation: https://learn.microsoft.com/python/api/azure-search-documents -- Azure RBAC for Search: https://learn.microsoft.com/azure/search/search-security-rbac -- DefaultAzureCredential: https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential - -## Summary - -The fix replaces manual REST API calls with the proper Azure Search SDK (`SearchIndexClient`), which correctly handles managed identity authentication for Azure AI Search. This aligns the test function with all other search operations in the codebase that already use the SDK approach successfully. From 7f8248ad7d8d12329dcb563f04672b24e7c07f96 Mon Sep 17 00:00:00 2001 From: vivche Date: Sat, 24 Jan 2026 09:25:23 -0500 Subject: [PATCH 26/35] Update application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../semantic_kernel_plugins/openapi_plugin_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py index f11f3d66..f1f1503c 100644 --- a/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py +++ b/application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py @@ -161,7 +161,7 @@ def _extract_auth_config(cls, config: Dict[str, Any]) -> Dict[str, Any]: # For bearer tokens stored as 'key' type if auth_type == 'key' and auth_method == 'bearer': token = auth_config.get('key', '') - debug_print(f"[Factory] Applying basic auth transformation") + debug_print(f"[Factory] Applying bearer auth transformation") return { 'type': 'bearer', 'token': token From a0fbffd0ea457ff21415474d3347e296fb1a9292 Mon Sep 17 00:00:00 2001 From: vivche Date: Sat, 24 Jan 2026 09:25:56 -0500 Subject: [PATCH 27/35] Update docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md index cb5045e4..8468778e 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md @@ -66,7 +66,7 @@ This created a chicken-and-egg problem: the agent couldn't be selected because i - Removed redundant single-agent loading logic 2. **`config.py`** (Line 91) - - Updated VERSION from "0.235.026" to "0.235.027" + - Updated VERSION from "0.236.011" to "0.236.012" ### Code Changes From 4bac07aca6099650f2ce28da4a5b7ffd34b3dd5c Mon Sep 17 00:00:00 2001 From: vivche Date: Sat, 24 Jan 2026 09:26:15 -0500 Subject: [PATCH 28/35] Update docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md index bcfac1ff..b9743b43 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -125,7 +125,7 @@ Group actions failed because: - **Parity achieved:** Both global and group routes now call `get_merged_plugin_settings()` 2. **`config.py`** - - Updated VERSION from "0.235.027" to "0.235.028" + - Updated VERSION from "0.236.011" to "0.236.012" ### Code Changes From 1c31db43bd1a168a701e10459b7ed8f712918763 Mon Sep 17 00:00:00 2001 From: vivche Date: Sat, 24 Jan 2026 09:26:41 -0500 Subject: [PATCH 29/35] Update docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md index 19a7407d..674350c3 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md @@ -71,7 +71,6 @@ Before creating the OAuth application, create a dedicated integration user: 1. **Log in to your ServiceNow instance** as an admin - URL: `https://devnnnnnn.service-now.com` -2. **Navigate to OAuth Application Registry:** 2. **Navigate to OAuth Application Registry:** ``` System OAuth > Application Registry From 31e8341eaca515060e5afc7981f95fdafd3f60f2 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Tue, 27 Jan 2026 08:17:43 -0500 Subject: [PATCH 30/35] Added ServiceNow support for create and publish article. Including readme for configuration for 2 agents. --- .../GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 10 +- .../ServiceNow/SERVICENOW_INTEGRATION.md | 44 +- .../ServiceNow/SERVICENOW_OAUTH_SETUP.md | 21 +- .../agents/ServiceNow/TWO_AGENT_SETUP.md | 542 ++++++++++++++++++ .../sample_now_knowledge_create_spec.yaml | 260 +++++++++ .../sample_now_knowledge_publish_spec.yaml | 264 +++++++++ ... => sample_now_knowledge_search_spec.yaml} | 12 +- ..._now_knowledge_search_spec_basicauth.yaml} | 0 ...enow_incident_api - basic auth sample.yaml | 2 +- .../servicenow_agent_instructions.txt | 33 ++ ...cenow_kb_management_agent_instructions.txt | 427 ++++++++++++++ 11 files changed, 1593 insertions(+), 22 deletions(-) create mode 100644 docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_publish_spec.yaml rename docs/how-to/agents/ServiceNow/open_api_specs/{sample_now_knowledge_latest_spec.yaml => sample_now_knowledge_search_spec.yaml} (93%) rename docs/how-to/agents/ServiceNow/open_api_specs/{sample_now_knowledge_latest_spec_basicauth.yaml => sample_now_knowledge_search_spec_basicauth.yaml} (100%) create mode 100644 docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt diff --git a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md index b9743b43..55a415f9 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md +++ b/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -28,7 +28,7 @@ When a group action was configured with OAuth bearer token authentication: [DEBUG] Auth type: bearer [DEBUG] Token available: True [DEBUG] Added bearer auth: EfP7otqXmV... -[DEBUG] Making request to https://dev222288.service-now.com/api/now/table/incident +[DEBUG] Making request to https://YOUR-INSTANCE.service-now.com/api/now/table/incident [DEBUG] Request headers: {'Authorization': 'Bearer EfP7otqXmV...', ...} [DEBUG] Response status: 401 [DEBUG] Response text: {"error":{"message":"User is not authenticated",...}} @@ -83,7 +83,7 @@ The fix revealed the actual data flow for authentication configuration: "auth": {"type": "key"}, "additionalFields": { "auth_method": "bearer", - "base_url": "https://dev222288.service-now.com/api/now" + "base_url": "https://YOUR-INSTANCE.service-now.com/api/now" } } ``` @@ -226,7 +226,7 @@ def get_merged_plugin_settings(plugin_type, current_settings, schema_dir): 2. Create new group action via UI: - Type: OpenAPI - Upload ServiceNow spec - - Base URL: `https://dev222288.service-now.com/api/now` + - Base URL: `https://YOUR-INSTANCE.service-now.com/api/now` - Authentication: **Bearer Token** (dropdown selection) - Token: `EfP7otqXmVmg06xfB9igagxL6Pjir7ewv99sZyMqYdzImlerPt9rHM1T1_L8cCEeWZAuWUV0GPDP2eZ56XWoEQ` 3. UI JavaScript sets `additionalFields.auth_method = 'bearer'` (line 1537) @@ -296,7 +296,7 @@ ServiceNow OAuth access tokens expire after a configured lifespan (e.g., 3,600 s **Example Failure:** ``` -Request: GET https://dev222288.service-now.com/api/now/table/incident +Request: GET https://YOUR-INSTANCE.service-now.com/api/now/table/incident Headers: Authorization: Bearer EfP7otqXmV... (expired token) Response: HTTP 401 - {"error":{"message":"User is not authenticated"}} ``` @@ -318,7 +318,7 @@ To make OAuth 2.0 authentication production-ready, Simple Chat needs to implemen "auth_type": "oauth2_client_credentials", "client_id": "565d53a80dfe4cb89b8869fd1d977308", "client_secret": "[encrypted_secret]", - "token_endpoint": "https://dev222288.service-now.com/oauth_token.do", + "token_endpoint": "https://YOUR-INSTANCE.service-now.com/oauth_token.do", "scope": "useraccount" } ``` diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md index 00e73fc5..fca2d04b 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -5,7 +5,16 @@ ## Overview -This guide documents the integration between Simple Chat and ServiceNow, enabling AI-powered incident management, ticket analysis, and support operations through natural language prompts. +This guide documents the **single-agent integration** between Simple Chat and ServiceNow, enabling AI-powered incident management and knowledge base search through natural language prompts. + +**This guide covers:** +- ✅ Incident management (create, update, query, statistics) +- ✅ Knowledge base search (read-only access) +- ✅ Single ServiceNow agent for standard support operations + +**For advanced KB management (create/publish articles), see:** +- 📘 [TWO_AGENT_SETUP.md](TWO_AGENT_SETUP.md) - Recommended approach with separate KB Management agent +- 📘 [KB_MULTI_ACTION_SETUP.md](KB_MULTI_ACTION_SETUP.md) - Alternative multi-action approach > **⚠️ Important - Work in Progress:** > This integration is under active development. **Check back regularly for updates** to the OpenAPI specifications and agent instructions. Unit testing of prompts is still in progress, so further changes to the spec files and agent instruction file are expected. @@ -14,9 +23,9 @@ This guide documents the integration between Simple Chat and ServiceNow, enablin ## Integration Architecture -**Approach:** Hybrid Integration +**Approach:** Single-Agent Hybrid Integration - **ServiceNow OpenAPI Actions** - Modular API integration for CRUD operations -- **ServiceNow Support Agent** - Specialized AI agent using those actions +- **ServiceNow Support Agent** - Specialized AI agent for incidents + read-only KB search --- @@ -60,7 +69,7 @@ Admin Password: [provided by ServiceNow] 2. Navigate to: **User Administration** → **Users** 3. Click **New** to create integration user: ``` - Username: simplechat6_integration + Username: simplechat6_servicenow_support_service First Name: Simple Last Name: Chat Integration Email: [your email] @@ -71,12 +80,23 @@ Admin Password: [provided by ServiceNow] - Navigate to **Roles** tab - Add roles: - `rest_api_explorer` - For REST API access - - `itil` - For incident management - - `knowledge` - For knowledge base access (optional) + - `itil` - For incident management (basic create/read/update) + - `knowledge` - For knowledge base read access + + **Optional Role for Enhanced Permissions:** + - `incident_manager` - Add only if you need to: + - Assign incidents to any user/group organization-wide + - Close and resolve any incident (not just your own) + - View all incidents across the organization + - Escalate incidents and modify any incident assignments + - Access incident analytics and reporting + + **Note:** For most AI agent use cases, `itil` + `knowledge` + `rest_api_explorer` is sufficient. 5. Set Password: - Click **Set Password** - Create secure password + - **Password needs reset: ☐ UNCHECK THIS** (important for API access) - Save for later use ### Step 3: Test REST API Access @@ -132,8 +152,8 @@ The integration uses two OpenAPI specification files that define all ServiceNow #### 2. Knowledge Base API **Files:** -- **Bearer Token Auth:** `sample_now_knowledge_latest_spec.yaml` (Recommended for production) -- **Basic Auth:** `sample_now_knowledge_latest_spec_basicauth.yaml` (For testing only) +- **Bearer Token Auth:** `sample_now_knowledge_search_spec.yaml` (Recommended for production) +- **Basic Auth:** `sample_now_knowledge_search_spec_basicauth.yaml` (For testing only) **Base URL:** `https://devXXXXX.service-now.com` @@ -153,13 +173,13 @@ The integration uses two OpenAPI specification files that define all ServiceNow #### Bearer Token Authentication (Production) - `sample_servicenow_incident_api.yaml` - Incident management with OAuth 2.0 bearer token -- `sample_now_knowledge_latest_spec.yaml` - Knowledge base search with OAuth 2.0 bearer token +- `sample_now_knowledge_search_spec.yaml` - Knowledge base search (read-only) with OAuth 2.0 bearer token - **Use these for:** Production deployments, secure enterprise environments - **Setup guide:** See `SERVICENOW_OAUTH_SETUP.md` for OAuth configuration #### Basic Authentication (Testing Only) - `sample_servicenow_incident_api_basicauth.yaml` - Incident management with username:password -- `sample_now_knowledge_latest_spec_basicauth.yaml` - Knowledge base search with username:password +- `sample_now_knowledge_search_spec_basicauth.yaml` - Knowledge base search (read-only) with username:password - **Use these for:** Initial testing, development instances, proof of concept - **Security note:** Not recommended for production use @@ -246,7 +266,7 @@ Name: servicenow_search_knowledge_base Display Name: ServiceNow - Search Knowledge Base Type: OpenAPI Description: Search knowledge articles with progressive fallback and retrieve full article content -OpenAPI Spec: [Upload sample_now_knowledge_latest_spec.yaml or sample_now_knowledge_latest_spec_basicauth.yaml] +OpenAPI Spec: [Upload sample_now_knowledge_search_spec.yaml or sample_now_knowledge_search_spec_basicauth.yaml] Base URL: https://devXXXXX.service-now.com Operations Included: @@ -479,7 +499,7 @@ The fix is included in Simple Chat v0.235.026+. The OpenAPI plugin factory now a #### Issue: "Authentication failed" error **Solution:** -- Verify username (simplechat6_integration) and password are correct +- Verify username (simplechat6_servicenow_support_service) and password are correct - Check integration user is active - Confirm user has `rest_api_explorer` role - Test credentials in REST API Explorer first diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md index 674350c3..8e92d4f6 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md @@ -189,9 +189,9 @@ You have two options to get an access token. > > The resulting token will execute API calls **as that integration user** with their specific roles and permissions. -#### **Option A: Using REST Client (Postman/Curl)** +#### **Option A: Using REST Client (Postman/cURL/PowerShell)** -**Request:** +**Using cURL:** ```bash curl -X POST https://devnnnnnn.service-now.com/oauth_token.do \ -H "Content-Type: application/x-www-form-urlencoded" \ @@ -202,6 +202,23 @@ curl -X POST https://devnnnnnn.service-now.com/oauth_token.do \ -d "password=YOUR_PASSWORD" # Integration user's password ``` +**Using PowerShell (recommended for Windows):** +```powershell +$response = Invoke-RestMethod -Uri "https://devnnnnnn.service-now.com/oauth_token.do" ` + -Method Post ` + -ContentType "application/x-www-form-urlencoded" ` + -Body @{ + grant_type="password" + client_id="YOUR_CLIENT_ID" # OAuth app Client ID + client_secret="YOUR_CLIENT_SECRET" # OAuth app Client Secret + username="YOUR_USERNAME" # ServiceNow integration user + password="YOUR_PASSWORD" # Integration user's password + } + +# Display the response +$response | ConvertTo-Json -Depth 10 +``` + **Response:** ```json { diff --git a/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md new file mode 100644 index 00000000..5d467ec7 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md @@ -0,0 +1,542 @@ +# ServiceNow Two-Agent Setup Guide (Advanced KB Management) + +**Version:** 0.236.012 +**Implemented in version:** 0.236.012 + +## Overview + +This guide describes the **advanced two-agent architecture** for organizations that need full knowledge base management capabilities in addition to standard incident management. + +**Use this approach when you need:** +- ✅ Separate permissions for KB creation and publishing +- ✅ Different ServiceNow roles for support staff vs. KB managers +- ✅ Ability to import external articles to ServiceNow KB +- ✅ Workflow-based KB article approval (draft → review → published) + +**For simpler single-agent setup (incidents + KB search only), see:** +- 📘 [SERVICENOW_INTEGRATION.md](SERVICENOW_INTEGRATION.md) - Recommended for most users + +--- + +## Architecture + +This setup uses **two ServiceNow agents** with different permission levels: + +1. **ServiceNow Support Agent** - Incident management + KB search (read-only) +2. **ServiceNow KB Management Agent** - Full KB operations (search, create, publish) + +Each agent uses a **separate ServiceNow integration user** with appropriate role assignments and **separate OAuth bearer tokens**. + +--- + +## Agent 1: ServiceNow Support Agent + +### Purpose +Primary agent for incident management and KB article searches. + +### Capabilities +- ✅ Create, update, query incidents +- ✅ Get incident statistics +- ✅ Search KB articles (read-only) +- ✅ View KB article details +- ❌ Cannot create KB articles +- ❌ Cannot publish KB articles + +### ServiceNow Integration User + +**Username:** `servicenow_support_service` +**Required ServiceNow Roles:** +- `itil` - Standard ITIL user access (basic incident read/write) +- `knowledge` - KB article read access +- `rest_api_explorer` - REST API access + +**Optional Role for Enhanced Permissions:** +- `incident_manager` - Adds ability to: + - Assign incidents to any user/group + - Close and resolve any incident + - View all incidents across the organization + - Escalate incidents + - Modify incident assignments and ownership + - Access incident analytics and reporting + +**Note:** For API-based incident operations, `itil` role is usually sufficient. Add `incident_manager` only if the agent needs to manage incidents assigned to other users. + +**Permissions:** +- Read/Write: `incident` table +- Read: `kb_knowledge` table (published articles only) +- Read: `kb_category` table +- Read: `kb_knowledge_base` table + +### Authentication +- **Type:** Bearer Token (OAuth 2.0) +- **Token Endpoint:** `https://YOUR-INSTANCE.service-now.com/oauth_token.do` +- **Grant Type:** Resource Owner Password Credentials + +### Actions Configuration + +**Action 1: Manage Incidents** +- OpenAPI Spec: `sample_servicenow_incident_api.yaml` +- Operations: queryIncidents, createIncident, getIncidentDetails, updateIncident, getIncidentStats +- Uses: `servicenow_support_service` credentials + +**Action 2: Search Knowledge Base** +- OpenAPI Spec: `sample_now_knowledge_search_spec.yaml` +- Operations: searchKnowledgeFacets, getKnowledgeArticle +- Uses: `servicenow_support_service` credentials + +### Agent Instructions +- File: `servicenow_agent_instructions.txt` +- Location: `docs/how-to/agents/ServiceNow/` + +### Available To +- All users (standard support staff) +- Primary agent for incident handling + +--- + +## Agent 2: ServiceNow KB Management Agent + +### Purpose +Specialized agent for knowledge base article management. + +### Capabilities +- ✅ Search KB articles +- ✅ View KB article details +- ✅ Create new KB articles from external URLs +- ✅ Publish draft articles +- ✅ Update existing articles +- ✅ Retire outdated articles +- ❌ Cannot manage incidents (use Support Agent for that) + +### ServiceNow Integration User + +**Username:** `simplechat6_servicenow_kb_manager` +**Required ServiceNow Roles:** +- `itil` - Standard ITIL user access +- `knowledge` - KB article contributor access +- `knowledge_manager` - Full KB management permissions (create, publish, retire) +- `rest_api_explorer` - REST API access + +**Alternative:** Use `knowledge_admin` instead of `knowledge_manager` for elevated permissions + +**Permissions:** +- Read: `kb_knowledge` table (all states: draft, review, published, retired) +- Create: `kb_knowledge` table +- Update: `kb_knowledge` table (including workflow_state changes) +- Read: `kb_category` table +- Read: `kb_knowledge_base` table + +### Authentication +- **Type:** Bearer Token (OAuth 2.0) - **SEPARATE TOKEN** +- **Token Endpoint:** `https://YOUR-INSTANCE.service-now.com/oauth_token.do` +- **Grant Type:** Resource Owner Password Credentials + +### Actions Configuration + +**Action 1: Search Knowledge Base** +- OpenAPI Spec: `sample_now_knowledge_search_spec.yaml` +- Operations: searchKnowledgeFacets, getKnowledgeArticle +- Uses: `simplechat6_servicenow_kb_manager` credentials + +**Action 2: Create Knowledge Articles** +- OpenAPI Spec: `sample_now_knowledge_create_spec.yaml` +- Operations: createKnowledgeArticle +- Uses: `simplechat6_servicenow_kb_manager` credentials + +**Action 3: Publish Knowledge Articles** +- OpenAPI Spec: `sample_now_knowledge_publish_spec.yaml` +- Operations: updateKnowledgeArticle +- Uses: `simplechat6_servicenow_kb_manager` credentials + +**Plugin: SmartHttpPlugin** (globally enabled by default) +- Enabled via: Admin Settings → "Enable HTTP Action" (enabled by default) +- Operation: get_web_content +- Used to fetch content from external URLs before creating KB articles +- No separate action configuration needed - available to all agents when globally enabled + +### Agent Instructions +- File: `servicenow_kb_management_agent_instructions.txt` +- Location: `docs/how-to/agents/ServiceNow/` + +### Available To +- Knowledge managers only +- Users who need to create and publish KB articles + +--- + +## Setup Steps + +### Step 1: Create ServiceNow Integration Users + +#### User 1: Support Agent Service Account + +``` +1. Log into ServiceNow as admin +2. Navigate to: User Administration > Users +3. Click "New" +4. Fill in: + - User ID: servicenow_support_service + - First name: ServiceNow Support + - Last name: Service Account + - Email: servicenow-support@your-domain.com + - Active: ✓ +5. Click "Submit" +6. Open the user record +7. Go to "Roles" tab +8. Add roles: itil, incident_manager +9. Save +``` + +#### User 2: KB Manager Service Account + +``` +1. Log into ServiceNow as admin +2. Navigate to: User Administration > Users +3. Click "New" +4. Fill in: + - User ID: simplechat6_servicenow_kb_manager + - First name: ServiceNow KB Manager + - Last name: Service Account + - Email: servicenow-kb@your-domain.com + - Active: ✓ + - Password needs reset: ☐ UNCHECK THIS (important for API access) +5. Click "Submit" +6. Set Password: + - Right-click the header bar > "Set Password" + - Enter a secure password + - Save the password for OAuth token generation +7. Open the user record +8. Go to "Roles" tab +9. Add roles: knowledge_manager, knowledge, itil, rest_api_explorer +10. Save +``` + +--- + +### Step 2: Generate OAuth Tokens + +> **📘 For detailed OAuth token generation instructions, see: [SERVICENOW_OAUTH_SETUP.md](SERVICENOW_OAUTH_SETUP.md)** +> +> The OAuth setup guide provides: +> - Complete OAuth application configuration in ServiceNow +> - Token generation using cURL, PowerShell, and Python +> - Token refresh procedures +> - Troubleshooting common OAuth issues + +#### Token 1: Support Agent + +Generate OAuth token for `servicenow_support_service` user: +- **Username:** servicenow_support_service +- **Client ID:** Your OAuth application client ID +- **Client Secret:** Your OAuth application client secret +- **Password:** Service account password + +**Save the access token** - You'll need it for the Support Agent action configuration. + +#### Token 2: KB Manager + +Generate OAuth token for `simplechat6_servicenow_kb_manager` user: +- **Username:** simplechat6_servicenow_kb_manager +- **Client ID:** Your OAuth application client ID +- **Client Secret:** Your OAuth application client secret +- **Password:** Service account password + +**Save the access token** - You'll need it for the KB Manager Agent action configuration. + +--- + +### Step 3: Configure Agent 1 (ServiceNow Support Agent) + +#### In Simple Chat Admin: + +``` +1. Navigate to: Admin > Personal Agents +2. Click "Create New Agent" +3. Fill in: + - Name: ServiceNow Support Agent + - Display Name: ServiceNow Support + - Description: Incident management and KB article searches + - Instructions: [Upload servicenow_agent_instructions.txt] + +4. Add Action 1: + - Name: Manage Incidents + - Type: OpenAPI Plugin + - Upload Spec: sample_servicenow_incident_api.yaml + - Authentication Type: Bearer Token + - Token: TOKEN_FOR_SUPPORT_AGENT + +5. Add Action 2: + - Name: Search Knowledge Base + - Type: OpenAPI Plugin + - Upload Spec: sample_now_knowledge_search_spec.yaml + - Authentication Type: Bearer Token + - Token: TOKEN_FOR_SUPPORT_AGENT + +6. Model: gpt-4o (recommended) +7. Availability: All Users +8. Save +``` + +--- + +### Step 4: Configure Agent 2 (ServiceNow KB Management Agent) + +#### In Simple Chat Admin: + +``` +1. Navigate to: Admin > Personal Agents +2. Click "Create New Agent" +3. Fill in: + - Name: ServiceNow KB Management Agent + - Display Name: ServiceNow KB Manager + - Description: Create and publish knowledge base articles from external URLs or direct content, search KB articles, and manage article lifecycle + - Instructions: [Upload servicenow_kb_management_agent_instructions.txt] + +4. Add Action 1 - Search Knowledge Base: + - Name: Search Knowledge Base + - Description: Search and retrieve knowledge base articles + - Type: OpenAPI Plugin + - Upload Spec: sample_now_knowledge_search_spec.yaml + - Authentication Type: Bearer Token + - Token: TOKEN_FOR_KB_MANAGER + +5. Add Action 2 - Create Knowledge Articles: + - Name: Create Knowledge Articles + - Description: Create new draft knowledge base articles from external content + - Type: OpenAPI Plugin + - Upload Spec: sample_now_knowledge_create_spec.yaml + - Authentication Type: Bearer Token + - Token: TOKEN_FOR_KB_MANAGER + +6. Add Action 3 - Publish Knowledge Articles: + - Name: Publish Knowledge Articles + - Description: Update and publish knowledge articles to make them visible to end users + - Type: OpenAPI Plugin + - Upload Spec: sample_now_knowledge_publish_spec.yaml + - Authentication Type: Bearer Token + - Token: TOKEN_FOR_KB_MANAGER + +7. Verify SmartHttpPlugin is enabled: + - Navigate to: Admin Settings → Plugins + - Ensure "Enable HTTP Action" is checked (enabled by default) + - This provides the get_web_content() function for fetching external URLs + +8. Model: gpt-4o (recommended) +9. Availability: Knowledge Managers Only (configure user groups) +10. Save +``` + +--- + +## Usage Examples + +### Example 1: Support Agent - Search KB and Create Incident + +**User to Support Agent:** +``` +"Search for email configuration KB articles and create an incident +for user John Doe who can't configure Outlook" +``` + +**Agent Executes:** +1. searchKnowledgeFacets(query="email configuration") +2. createIncident(description="Email configuration issue", assigned_to="John Doe") + +**Agent Response:** +``` +Found 3 KB articles about email configuration: + +KB0011 - Configure email settings in Microsoft 365 +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123 + +✅ Incident INC0012345 created successfully. + +Priority: 3 - Moderate +Assigned to: John Doe +Category: Email +Status: New + +Related KB articles have been added to the incident notes. +``` + +--- + +### Example 2: KB Manager - Create and Publish Article + +**User to KB Manager:** +``` +"Add this Microsoft article to ServiceNow KB and publish it: +https://support.microsoft.com/en-us/office/configure-email-settings" +``` + +**Agent Executes:** +1. SmartHttpPlugin.get_web_content(url="https://support.microsoft.com...") +2. createKnowledgeArticle(content=extracted, workflow_state="draft") +3. User confirms → updateKnowledgeArticle(workflow_state="published") + +**Agent Response:** +``` +✅ Knowledge article KB0001234 created and published successfully. + +**Article Details:** +- Title: Configure email settings in Microsoft 365 +- Category: Email +- Type: How-to guide +- Source: https://support.microsoft.com/en-us/office/configure-email-settings +- Content: 5,247 characters + +**Live Article:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 + +The article is now visible to all users. +``` + +--- + +### Example 3: KB Manager - Publish Existing Draft + +**User to KB Manager:** +``` +"Publish KB article KB0001234" +``` + +**Agent Executes:** +1. searchKnowledgeFacets(query="number=KB0001234") +2. updateKnowledgeArticle(sys_id="abc123", workflow_state="published") + +**Agent Response:** +``` +✅ Knowledge article KB0001234 published successfully. + +**Publication Details:** +- Title: Configure email settings in Microsoft 365 +- Status: Draft → Published ✓ +- Published: Jan 26, 2026 at 3:30 PM + +**Live Article:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 +``` + +--- + +## Security Model + +### Permission Separation + +| Operation | Support Agent | KB Manager | +|-----------|---------------|------------| +| Search KB articles | ✅ Yes | ✅ Yes | +| View KB article details | ✅ Yes | ✅ Yes | +| Create incidents | ✅ Yes | ❌ No | +| Update incidents | ✅ Yes | ❌ No | +| Create KB articles | ❌ No | ✅ Yes | +| Publish KB articles | ❌ No | ✅ Yes | +| Update KB articles | ❌ No | ✅ Yes | +| Retire KB articles | ❌ No | ✅ Yes | + +### Why Two Separate Integration Users? + +1. **Least Privilege:** Each agent has only the permissions it needs +2. **Audit Trail:** Different service accounts show who did what in ServiceNow audit logs +3. **Security:** Support Agent cannot accidentally publish unapproved KB articles +4. **Compliance:** Enforces approval workflow (create draft → manager publishes) +5. **Token Isolation:** If one token is compromised, other agent remains secure + +--- + +## Troubleshooting + +### Problem: Support Agent Cannot Search KB Articles + +**Symptoms:** 401 Unauthorized or 403 Forbidden when searching KB + +**Solution:** +1. Verify `servicenow_support_service` has `itil` role in ServiceNow +2. Check bearer token is valid and not expired +3. Verify `sample_now_knowledge_search_spec.yaml` base URL matches your instance + +--- + +### Problem: KB Manager Cannot Publish Articles + +**Symptoms:** 403 Forbidden when calling updateKnowledgeArticle + +**Solution:** +1. Verify `simplechat6_servicenow_kb_manager` has `knowledge_manager` role +2. Check that separate bearer token is configured (not using same token as Support Agent) +3. Verify article exists and is in valid state for publishing + +--- + +### Problem: External URL Content Not Fetching + +**Symptoms:** "Unable to fetch content from URL" error + +**Solution:** +1. Verify SmartHttpPlugin action is configured for KB Manager agent +2. Check URL is accessible from Simple Chat server +3. Try simpler URL first (e.g., public Microsoft docs) +4. Check for firewall or proxy blocking external requests + +--- + +## Maintenance + +### Token Refresh + +Bearer tokens expire after a period (typically 1 hour). + +> **📘 For token refresh procedures, see: [SERVICENOW_OAUTH_SETUP.md](SERVICENOW_OAUTH_SETUP.md)** + +After obtaining a new token, update it in the agent action configuration in Simple Chat. + +### Role Updates + +If ServiceNow roles change: +1. Update integration user roles in ServiceNow +2. Regenerate OAuth token +3. Update token in Simple Chat agent configuration +4. Test agent operations + +--- + +## Files Reference + +### Agent Instructions +- `servicenow_agent_instructions.txt` - Support Agent (incidents + KB search) +- `servicenow_kb_management_agent_instructions.txt` - KB Manager (full KB operations) + +### OpenAPI Specs +- `sample_servicenow_incident_api.yaml` - Incident management operations +- `sample_now_knowledge_search_spec.yaml` - KB search (read-only) +- `sample_now_knowledge_search_spec.yaml` - KB search operations +- `sample_now_knowledge_create_spec.yaml` - KB creation operations +- `sample_now_knowledge_publish_spec.yaml` - KB publish/update operations + +### Documentation +- `KB_MULTI_ACTION_SETUP.md` - Original multi-action approach (reference) +- `TWO_AGENT_SETUP.md` - This file (recommended approach) + +--- + +## Benefits of Two-Agent Approach + +1. ✅ **Clear Separation:** Each agent has distinct purpose +2. ✅ **Better UX:** Users choose appropriate agent for their task +3. ✅ **Security:** Permission enforcement at ServiceNow API level +4. ✅ **Audit Trail:** Different service accounts for different operations +5. ✅ **Flexibility:** Easy to add more agents with different permission levels +6. ✅ **Scalability:** Can create specialized agents for other ServiceNow tables +7. ✅ **Token Security:** Separate tokens limit blast radius if one is compromised + +--- + +## Next Steps + +1. ✅ Create two ServiceNow integration users +2. ✅ Generate separate OAuth tokens +3. ✅ Configure Support Agent with incident + KB search actions +4. ✅ Configure KB Manager Agent with full KB management actions +5. ✅ Test both agents with sample scenarios +6. ✅ Train users on which agent to use for which tasks +7. ✅ Monitor ServiceNow audit logs for service account activity diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml new file mode 100644 index 00000000..e40a9c40 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml @@ -0,0 +1,260 @@ +openapi: 3.0.1 +info: + title: ServiceNow Knowledge Base Create API + description: | + ServiceNow Knowledge Management REST API for CREATING knowledge articles. + + USE THIS SPEC FOR: Knowledge contributors who can create draft articles + PERMISSIONS REQUIRED: ServiceNow 'knowledge' role + + This spec contains only POST operation for creating articles in DRAFT state. + Articles created with this API require approval before publishing. + For publishing articles, use sample_now_knowledge_publish_spec.yaml + For searching articles, use sample_now_knowledge_search_spec.yaml + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://YOUR-INSTANCE.service-now.com + description: ServiceNow instance (replace YOUR-INSTANCE with your instance name) + +security: + - bearerAuth: [] + +paths: + /api/now/table/kb_knowledge: + post: + operationId: createKnowledgeArticle + summary: Create a new knowledge base article + description: | + Create a new knowledge base article in ServiceNow. + Use this function to add external content, documentation, or support articles to the knowledge base. + Articles are created in 'draft' state by default for review before publication. + + TYPICAL USE CASES: + - Import content from external URLs (Microsoft docs, vendor support articles, etc.) + - Create KB articles from email conversations or chat transcripts + - Document solutions discovered during incident resolution + - Add standardized procedures and how-to guides + + WORKFLOW FOR EXTERNAL URL CONTENT: + 1. Use SmartHttpPlugin.get_web_content to fetch and extract content from URL + 2. Clean extracted text (remove ads, navigation, footers - SmartHttpPlugin does this) + 3. Create article with extracted content using this function + 4. Always set source_url to attribute the original source + 5. Create in 'draft' state for review before publishing + + REQUIRED FIELDS: + - short_description: Article title (max 160 characters) + - text: Article body content (HTML supported) + - kb_knowledge_base: Target knowledge base (e.g., "IT Support") + + RECOMMENDED FIELDS: + - kb_category: Category for organization (e.g., "Email", "Network") + - article_type: Type of article ("how-to", "reference", "troubleshooting") + - source_url: Original URL for attribution + - workflow_state: Publication state (use "draft" for review) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - short_description + - text + - kb_knowledge_base + properties: + short_description: + type: string + description: Article title (max 160 characters) + maxLength: 160 + example: "How to configure Microsoft 365 email settings" + + text: + type: string + description: | + Article content/body. HTML is supported. + For external URL content, include cleaned extracted text. + Max recommended length: 75,000 characters. + example: "

Email Configuration Steps

\\n

To configure Microsoft 365 email...

" + + kb_knowledge_base: + type: string + description: | + Knowledge base name or sys_id. + Common values: "IT Support", "HR", "Facilities" + example: "IT Support" + + kb_category: + type: string + description: | + Category for organization and filtering. + Common values: "Email", "Network", "Hardware", "Software" + example: "Email" + + article_type: + type: string + description: Type of article + enum: ["how-to", "reference", "troubleshooting", "faq", "general"] + example: "how-to" + + workflow_state: + type: string + description: | + Publication state. Use 'draft' for new articles to allow review. + States: draft, review, published, retired + enum: ["draft", "review", "published", "retired"] + default: "draft" + example: "draft" + + source_url: + type: string + description: | + Original source URL for attribution. + ALWAYS include this when creating articles from external URLs. + example: "https://support.microsoft.com/en-us/office/configure-email-settings" + + author: + type: string + description: | + Article author name or sys_id. + If not specified, defaults to the authenticated user. + example: "John Doe" + + valid_to: + type: string + format: date-time + description: | + Article expiration date (optional). + Use ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ + example: "2027-12-31T23:59:59Z" + + responses: + '201': + description: Knowledge article created successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/KnowledgeArticle' + example: + result: + sys_id: "abc123xyz789" + number: "KB0001234" + short_description: "How to configure Microsoft 365 email settings" + text: "

Email Configuration Steps

..." + kb_category: "Email" + kb_knowledge_base: "IT Support" + workflow_state: "draft" + source_url: "https://support.microsoft.com/en-us/office/configure-email-settings" + article_type: "how-to" + sys_created_on: "2026-01-26T10:30:00Z" + + '400': + description: Bad request - missing required fields or invalid data + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + detail: + type: string + example: + error: + message: "Missing required field: kb_knowledge_base" + detail: "The kb_knowledge_base field is required for creating knowledge articles" + + '401': + description: Unauthorized - Invalid credentials + + '403': + description: Forbidden - Insufficient permissions to create knowledge articles + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant. + + schemas: + KnowledgeArticle: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Knowledge article number + example: "KB0001234" + + short_description: + type: string + description: Article title/summary + example: "How to configure email server settings" + + text: + type: string + description: Article content/body + example: "To configure email server settings, follow these steps: 1. Navigate to..." + + kb_category: + type: string + description: Knowledge category + example: "Email" + + kb_knowledge_base: + type: string + description: Knowledge base name + example: "IT Support" + + author: + type: string + description: Article author + example: "John Doe" + + workflow_state: + type: string + description: "Publication state: draft, review, published, retired" + example: "draft" + + sys_view_count: + type: string + description: Number of times article was viewed + example: "0" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2026-01-26T10:30:00Z" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-26T10:30:00Z" + + source_url: + type: string + description: Original source URL + example: "https://support.microsoft.com/article" + + article_type: + type: string + description: Type of article (how-to, troubleshooting, reference, etc.) + example: "how-to" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_publish_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_publish_spec.yaml new file mode 100644 index 00000000..273cf092 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_publish_spec.yaml @@ -0,0 +1,264 @@ +openapi: 3.0.1 +info: + title: ServiceNow Knowledge Base Publish API + description: | + ServiceNow Knowledge Management REST API for UPDATING and PUBLISHING knowledge articles. + + USE THIS SPEC FOR: Knowledge managers who can publish and update articles + PERMISSIONS REQUIRED: ServiceNow 'knowledge_manager' or 'knowledge_admin' role + + This spec contains PATCH operation for updating articles and changing workflow states. + Use this to publish draft articles, update content, or retire outdated articles. + For creating articles, use sample_now_knowledge_create_spec.yaml + For searching articles, use sample_now_knowledge_search_spec.yaml + version: 1.0.0 + contact: + name: ServiceNow API Support + url: https://developer.servicenow.com + +servers: + - url: https://YOUR-INSTANCE.service-now.com + description: ServiceNow instance (replace YOUR-INSTANCE with your instance name) + +security: + - bearerAuth: [] + +paths: + /api/now/table/kb_knowledge/{sys_id}: + patch: + operationId: updateKnowledgeArticle + summary: Update existing knowledge article (including publish) + description: | + Update an existing knowledge base article in ServiceNow. + Use this function to modify article content, change workflow state, or publish draft articles. + + COMMON USE CASES: + - Publish draft articles (workflow_state: "draft" → "published") + - Update article content or title + - Change category or knowledge base + - Retire outdated articles (workflow_state: "published" → "retired") + - Move to review state (workflow_state: "draft" → "review") + + WORKFLOW STATE TRANSITIONS: + - draft → review (submit for approval) + - review → published (approve and publish) + - draft → published (direct publish - requires elevated permissions) + - published → retired (retire outdated content) + - retired → published (restore retired article) + + PERMISSIONS: + - Updating draft articles: Requires 'knowledge' role + - Publishing articles: Requires 'knowledge_manager' or 'knowledge_admin' role + - Retiring articles: Requires 'knowledge_manager' or 'knowledge_admin' role + parameters: + - name: sys_id + in: path + required: true + schema: + type: string + description: The sys_id of the knowledge article to update + example: "abc123xyz789" + + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + workflow_state: + type: string + description: | + Update publication state. + Common transitions: draft→review, review→published, draft→published, published→retired + enum: ["draft", "review", "published", "retired"] + example: "published" + + short_description: + type: string + description: Update article title + maxLength: 160 + example: "Updated: How to configure Microsoft 365 email settings" + + text: + type: string + description: Update article content/body (HTML supported) + example: "

Updated Email Configuration Steps

\\n

Updated content...

" + + kb_category: + type: string + description: Change article category + example: "Email" + + kb_knowledge_base: + type: string + description: Move to different knowledge base + example: "IT Support" + + article_type: + type: string + description: Update article type + enum: ["how-to", "reference", "troubleshooting", "faq", "general"] + example: "how-to" + + valid_to: + type: string + format: date-time + description: Set or update expiration date + example: "2027-12-31T23:59:59Z" + examples: + publishArticle: + summary: Publish a draft article + value: + workflow_state: "published" + updateContent: + summary: Update article content and title + value: + short_description: "Updated: Email configuration guide" + text: "

Updated content

New instructions...

" + retireArticle: + summary: Retire outdated article + value: + workflow_state: "retired" + + responses: + '200': + description: Article updated successfully + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/KnowledgeArticle' + example: + result: + sys_id: "abc123xyz789" + number: "KB0001234" + short_description: "How to configure Microsoft 365 email settings" + workflow_state: "published" + sys_updated_on: "2026-01-26T15:30:00Z" + + '400': + description: Bad request - invalid workflow transition or data + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + detail: + type: string + example: + error: + message: "Invalid workflow state transition" + detail: "Cannot transition from 'retired' to 'draft' directly" + + '401': + description: Unauthorized - Invalid credentials + + '403': + description: Forbidden - Insufficient permissions to publish/update articles + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + detail: + type: string + example: + error: + message: "Access Denied" + detail: "Publishing articles requires 'knowledge_manager' role" + + '404': + description: Article not found + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant. + + schemas: + KnowledgeArticle: + type: object + properties: + sys_id: + type: string + description: Unique system identifier + example: "abc123xyz789" + + number: + type: string + description: Knowledge article number + example: "KB0001234" + + short_description: + type: string + description: Article title/summary + example: "How to configure email server settings" + + text: + type: string + description: Article content/body + example: "To configure email server settings, follow these steps: 1. Navigate to..." + + kb_category: + type: string + description: Knowledge category + example: "Email" + + kb_knowledge_base: + type: string + description: Knowledge base name + example: "IT Support" + + author: + type: string + description: Article author + example: "John Doe" + + workflow_state: + type: string + description: "Publication state: draft, review, published, retired" + example: "published" + + sys_view_count: + type: string + description: Number of times article was viewed + example: "145" + + sys_created_on: + type: string + format: date-time + description: Creation timestamp + example: "2025-06-15T10:30:00Z" + + sys_updated_on: + type: string + format: date-time + description: Last update timestamp + example: "2026-01-26T14:22:00Z" + + valid_to: + type: string + format: date-time + description: Article expiration date + example: "2027-12-31T23:59:59Z" + + article_type: + type: string + description: Type of article (how-to, troubleshooting, reference, etc.) + example: "how-to" diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_search_spec.yaml similarity index 93% rename from docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml rename to docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_search_spec.yaml index 9cd76173..a5f84418 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_search_spec.yaml @@ -1,7 +1,15 @@ openapi: 3.0.1 info: - title: ServiceNow Knowledge Base API - description: ServiceNow Knowledge Management REST API for searching and retrieving knowledge articles - Optimized for Simple Chat integration + title: ServiceNow Knowledge Base Search API (Read-Only) + description: | + ServiceNow Knowledge Management REST API for SEARCHING and READING knowledge articles only. + + USE THIS SPEC FOR: All users who need to search and view KB articles + PERMISSIONS REQUIRED: ServiceNow 'itil' role (standard user access) + + This spec contains ONLY GET operations - no create, update, or publish capabilities. + For knowledge article creation, use sample_now_knowledge_create_spec.yaml + For knowledge article publishing, use sample_now_knowledge_publish_spec.yaml version: 1.0.0 contact: name: ServiceNow API Support diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_search_spec_basicauth.yaml similarity index 100% rename from docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_latest_spec_basicauth.yaml rename to docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_search_spec_basicauth.yaml diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml index d6425364..4826b08a 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_incident_api - basic auth sample.yaml @@ -8,7 +8,7 @@ info: url: https://developer.servicenow.com servers: - - url: https://dev222288.service-now.com/api/now + - url: https://YOUR-INSTANCE.service-now.com/api/now description: ServiceNow Developer Instance security: diff --git a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt index cf181939..fca6ba58 100644 --- a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt +++ b/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt @@ -24,6 +24,8 @@ From the ServiceNow incident management action (Manage Incidents): From the ServiceNow knowledge base search action (Search Knowledge Base): - searchKnowledgeFacets - Search KB articles with progressive search - getKnowledgeArticle - Retrieve complete article content by sys_id +- READ-ONLY access - cannot create or publish articles +- For KB creation/publishing, use the dedicated KB Management agent **Error Handling:** - If an action fails, report the error clearly without technical jargon @@ -259,4 +261,35 @@ KEYWORD EXTRACTION: - Do not be alarmed if work_notes field appears empty immediately after update - The update was successful - ServiceNow processes journal entries asynchronously +**CRITICAL - Knowledge Base Article URL Formatting:** +⚠️ ALWAYS USE THE MODERN SERVICENOW KB URL FORMAT ⚠️ + +When displaying KB article references in incident details, comments, or responses: + +DO NOT use legacy URL format: +❌ https://servicenow.com/kb_view.do?sys_id=ARTICLE_SYS_ID +❌ https://INSTANCE.service-now.com/kb_view.do?sys_id=ARTICLE_SYS_ID + +ALWAYS use modern Knowledge Center URL format: +✅ https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ARTICLE_SYS_ID + +Where: +- INSTANCE = Your ServiceNow instance name (e.g., devnnnnnn, yourcompany, prod123) +- ARTICLE_SYS_ID = The sys_id of the KB article (e.g., 3b0785718703210deddb882a2e3ec20) + +**Example:** +If incident INC0010119 references KB article KB0000017 with sys_id 3b0785718703210deddb882a2e3ec20 on instance devnnnnnn: + +CORRECT URL: +https://devnnnnnn.service-now.com/now/knowledge-center/kb_view/kb_knowledge/3b0785718703210deddb882a2e3ec20 + +DISPLAY FORMAT: +Comments: 2026-01-26 18:29:37 - Simple Chart6 (Comments) Related KB article: KB0000017 - What is the Windows key? +https://devnnnnnn.service-now.com/now/knowledge-center/kb_view/kb_knowledge/3b0785718703210deddb882a2e3ec20 + +**Extracting Instance Name:** +- From your ServiceNow API base URL: https://INSTANCE.service-now.com/api/now +- Extract the INSTANCE part (before .service-now.com) +- Use this INSTANCE in all KB article URLs you generate + **Key Principle: Results first, no process narration. Honor user's limits exactly.** diff --git a/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt b/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt new file mode 100644 index 00000000..e4acdce5 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt @@ -0,0 +1,427 @@ +You are a ServiceNow Knowledge Base Manager with full access to KB operations. Execute actions and show results immediately without narration. + +**CRITICAL: DO NOT narrate what you're doing. Just execute and show data.** + +❌ NEVER say: +- "I will search..." +- "Creating article..." +- "Publishing now..." +- "Proceeding with..." +- "Let me check..." +- "I'll fetch..." + +✅ INSTEAD: Execute the action silently and return only the results. + +**You have these operations - use them immediately:** + +From the ServiceNow knowledge base action (Knowledge Base Management): +- searchKnowledgeFacets - Search KB articles with progressive search +- getKnowledgeArticle - Retrieve complete article content by sys_id +- createKnowledgeArticle - Create new KB articles in draft state +- updateKnowledgeArticle - Update/publish existing KB articles + +**Authentication:** +This agent uses a ServiceNow integration user with 'knowledge_manager' role. +Permissions: Full KB access (search, create, update, publish) + +**Execution Pattern:** + +For READ operations (search, get article): +1. Execute action immediately (no announcement) +2. Return formatted results +3. Add brief analysis if requested + +For WRITE operations (create, update, publish): +1. Confirm required parameters if missing +2. Execute after confirmation +3. Return success message with article preview/details + +**CRITICAL - Knowledge Base Progressive Search:** +⚠️ PROGRESSIVE SEARCH REQUIRES MULTIPLE FUNCTION CALLS ⚠️ + +You MUST make TWO separate searchKnowledgeFacets calls when first search returns 0 results: + +CALL 1 - Try Exact Phrase: +1. Call searchKnowledgeFacets(sysparm_query="textLIKEemail delivery troubleshooting") +2. Check if result count = 0 +3. If count > 0: Return results, DONE ✓ +4. If count = 0: Proceed to CALL 2 (do not give up!) + +CALL 2 - Broad Keyword Fallback: +1. Extract primary keyword from phrase (e.g., "email" from "email delivery troubleshooting") +2. Call searchKnowledgeFacets(sysparm_query="textLIKEemail") <-- NEW FUNCTION CALL +3. Return whatever results are found (likely 5+ articles) + +WHY THIS MATTERS: +- Exact phrase "email delivery troubleshooting" = 0 results (no article has this exact wording) +- Broad keyword "email" = 5+ results (KB0000011, KB0000024, KB0000028, etc.) +- You MUST make the second function call when first returns 0 + +DO NOT: +❌ Give up after first search returns 0 +❌ Say "no articles found" without trying broad keyword +❌ Use complex OR queries in first attempt - keep it simple + +KEYWORD EXTRACTION: +- "email delivery troubleshooting" → primary keyword: "email" +- "spam filter blocking" → primary keyword: "spam" +- "network connectivity issues" → primary keyword: "network" + +**CRITICAL - Knowledge Base Article URL Formatting:** +⚠️ ALWAYS USE THE MODERN SERVICENOW KB URL FORMAT ⚠️ + +When displaying KB article references: + +DO NOT use legacy URL format: +❌ https://servicenow.com/kb_view.do?sys_id=ARTICLE_SYS_ID +❌ https://INSTANCE.service-now.com/kb_view.do?sys_id=ARTICLE_SYS_ID + +ALWAYS use modern Knowledge Center URL format: +✅ https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ARTICLE_SYS_ID + +Where: +- INSTANCE = Your ServiceNow instance name (e.g., YOUR-INSTANCE, yourcompany, prod123) +- ARTICLE_SYS_ID = The sys_id of the KB article (e.g., 3b0785718703210deddb882a2e3ec20) + +**Extracting Instance Name:** +- From your ServiceNow API base URL: https://INSTANCE.service-now.com/api/now +- Extract the INSTANCE part (before .service-now.com) +- Use this INSTANCE in all KB article URLs you generate + +**CRITICAL - Creating Knowledge Base Articles from External URLs:** +⚠️ TWO-STEP WORKFLOW: FETCH CONTENT → CREATE ARTICLE ⚠️ + +When user asks to add external URL content to ServiceNow KB: + +STEP 1 - Fetch External Content: +1. Use SmartHttpPlugin.get_web_content(url="https://external-url.com") action +2. SmartHttpPlugin automatically: + - Downloads HTML, JSON, or PDF content + - Extracts clean text using BeautifulSoup + - Removes ads, navigation, footers + - Limits content to 75,000 characters +3. Verify content was extracted successfully +4. Extract title from content (usually first H1/H2 heading) + +STEP 2 - Create Knowledge Article: +1. Call createKnowledgeArticle with extracted content +2. Map external content to KB fields: + - short_description: Use extracted title (max 160 chars) + - text: Use extracted content from SmartHttpPlugin + - kb_knowledge_base: "IT Support" (or user-specified) + - kb_category: Determine from content (Email, Network, Hardware, Software) + - article_type: "reference" (for external docs) or "how-to" (for guides) + - workflow_state: "draft" (ALWAYS create in draft for review) + - source_url: Original URL for attribution (REQUIRED) +3. Return success message with article number and review link + +STEP 3 - REQUIRED: Provide Article Preview and Review Link: +After creating the article, you MUST provide: +1. ✅ Success confirmation with article number +2. 📋 Article details preview: + - Title (short_description) + - Category + - Article type + - Source URL + - Content length (character count or word count) +3. 🔗 Direct review link using modern Knowledge Center URL format: + - https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ARTICLE_SYS_ID +4. 📝 Draft state reminder - explain that article needs review before publishing + +DO NOT just say "Article created" - ALWAYS provide the full preview with clickable review link. + +**Example Create Workflow:** + +User request: "Add this Microsoft support article to ServiceNow KB: https://support.microsoft.com/en-us/office/configure-email-settings" + +Your execution: + +CALL 1 - SmartHttpPlugin.get_web_content: +```json +{ + "url": "https://support.microsoft.com/en-us/office/configure-email-settings" +} +``` + +Result: +```json +{ + "content": "Configure email settings in Microsoft 365\n\nTo configure email settings...\n\nStep 1: Sign in to your account...\nStep 2: Navigate to Settings...\n[extracted clean text, ~5000 chars]", + "title": "Configure email settings in Microsoft 365" +} +``` + +CALL 2 - createKnowledgeArticle: +```json +{ + "short_description": "Configure email settings in Microsoft 365", + "text": "

Configure email settings in Microsoft 365

\n

To configure email settings...

\n

Step 1: Sign in to your account...

\n

Step 2: Navigate to Settings...

", + "kb_knowledge_base": "IT Support", + "kb_category": "Email", + "article_type": "how-to", + "workflow_state": "draft", + "source_url": "https://support.microsoft.com/en-us/office/configure-email-settings" +} +``` + +Result: +```json +{ + "result": { + "sys_id": "abc123xyz789", + "number": "KB0001234", + "short_description": "Configure email settings in Microsoft 365", + "workflow_state": "draft" + } +} +``` + +Your response to user: +✅ "Knowledge article KB0001234 created successfully in draft state. + +**Article Details:** +- Title: Configure email settings in Microsoft 365 +- Category: Email +- Type: How-to guide +- Source: https://support.microsoft.com/en-us/office/configure-email-settings +- Content length: ~5,000 characters + +**Review & Publish:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 + +📝 The article is in draft state. You can review and publish it now, or I can publish it for you." + +**Content Extraction Quality:** +- SmartHttpPlugin handles HTML, JSON, and PDF formats +- For PDFs, it uses Azure Document Intelligence for text extraction +- For HTML, it uses BeautifulSoup to extract main content +- Content is automatically cleaned (no ads, nav menus, footers) +- If content exceeds 75,000 characters, it's truncated with note + +**Article Creation Best Practices:** +- ALWAYS create in "draft" state first (allows review before publishing) +- ALWAYS include source_url for attribution and traceability +- Choose appropriate kb_category based on content topic +- Use article_type="reference" for vendor docs, "how-to" for guides +- Extract meaningful title from content (first heading or page title) +- If SmartHttpPlugin extraction fails, report error clearly + +**CRITICAL - Publishing Knowledge Base Articles:** +⚠️ YOU HAVE FULL PUBLISHING PERMISSIONS ⚠️ + +When user asks to publish a KB article: + +STEP 1 - Verify Article Exists (if only article number provided): +1. If user provides just article number (e.g., "KB0001234"): + - Call searchKnowledgeFacets(sysparm_query="number=KB0001234") + - Extract sys_id from result +2. If user provides sys_id directly, skip to STEP 2 + +STEP 2 - Update Article to Published State: +1. Call updateKnowledgeArticle(sys_id="abc123...", workflow_state="published") +2. Verify response shows workflow_state="published" + +STEP 3 - Confirm Publication: +After publishing, provide: +1. ✅ Success confirmation with article number +2. 📋 Publication details: + - Article title + - Previous state (draft/review) → published + - Publication timestamp +3. 🔗 Live article link using modern Knowledge Center URL format + +**Example Publishing Workflow:** + +User request: "Publish KB article KB0001234" + +Your execution: + +CALL 1 - searchKnowledgeFacets (to get sys_id): +```json +{ + "sysparm_query": "number=KB0001234", + "sysparm_fields": "sys_id,number,short_description,workflow_state" +} +``` + +Result: +```json +{ + "result": [ + { + "sys_id": "abc123xyz789", + "number": "KB0001234", + "short_description": "Configure email settings in Microsoft 365", + "workflow_state": "draft" + } + ] +} +``` + +CALL 2 - updateKnowledgeArticle: +```json +{ + "sys_id": "abc123xyz789", + "workflow_state": "published" +} +``` + +Result: +```json +{ + "result": { + "sys_id": "abc123xyz789", + "number": "KB0001234", + "short_description": "Configure email settings in Microsoft 365", + "workflow_state": "published", + "sys_updated_on": "2026-01-26T15:30:00Z" + } +} +``` + +Your response to user: +✅ "Knowledge article KB0001234 published successfully. + +**Publication Details:** +- Title: Configure email settings in Microsoft 365 +- Status: Draft → Published ✓ +- Published: Jan 26, 2026 at 3:30 PM + +**Live Article:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 + +The article is now visible to all users." + +**CRITICAL - Combined Workflow: Create and Publish:** +⚠️ YOU CAN CREATE AND PUBLISH IN ONE REQUEST ⚠️ + +When user asks to "add article and publish" or "create and publish KB": + +OPTION 1 - Create in Published State (Direct): +```json +{ + "short_description": "Article title", + "text": "

Content

...", + "kb_knowledge_base": "IT Support", + "kb_category": "Email", + "workflow_state": "published", + "source_url": "https://..." +} +``` + +OPTION 2 - Create Draft Then Publish (Safer): +1. Create article in draft state +2. Immediately publish using updateKnowledgeArticle +3. This allows verification before publishing + +**Recommended approach:** Create in draft first, preview for user, then publish if confirmed. + +**Other Update Operations:** + +Update article content: +```json +{ + "sys_id": "abc123xyz789", + "short_description": "Updated title", + "text": "

Updated content

New instructions...

" +} +``` + +Your response format: +✅ "Knowledge article KB0001234 updated successfully. + +**Changes:** +- Updated title and content +- Last modified: Jan 26, 2026 at 3:45 PM + +**View Updated Article:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789" + +Retire outdated article: +```json +{ + "sys_id": "abc123xyz789", + "workflow_state": "retired" +} +``` + +Your response format: +✅ "Knowledge article KB0001234 retired successfully. + +**Status:** +- Title: Configure email settings in Microsoft 365 +- Status: Published → Retired +- Retired: Jan 26, 2026 at 4:00 PM + +The article is no longer visible to users but remains in the system for reference." + +**Workflow State Transitions:** +You can perform any of these transitions: +- draft → review (submit for approval) +- draft → published (direct publish) +- review → published (approve and publish) +- review → draft (send back for revision) +- published → retired (retire outdated content) +- retired → published (restore retired article) + +**Publishing Best Practices:** +- Always verify article exists and get correct sys_id before publishing +- Check current workflow_state before attempting state transitions +- Invalid transitions (e.g., retired → draft directly) may return 400 error +- You have knowledge_manager role - you can publish directly +- Provide clear confirmation with live article link after publishing + +**Error Handling:** +- If SmartHttpPlugin fails to fetch content: + - Report: "Unable to fetch content from URL. The site may be blocked or unavailable." + - Do not attempt to create KB article without content +- If createKnowledgeArticle fails: + - Check for missing required fields (short_description, text, kb_knowledge_base) + - Report error clearly with recommended action +- If updateKnowledgeArticle fails: + - Verify article exists (search first) + - Check for invalid workflow state transition + - Report error with current state and suggested next steps + +**Response Format Examples:** + +Search request: "Find KB articles about email" +❌ Bad: "I'll search for email KB articles..." +✅ Good: [Execute search] "Found 5 KB articles about email: + +| Number | Title | Category | Views | +|--------|-------|----------|-------| +| KB0011 | Configure email settings | Email | 145 | +| KB0024 | Troubleshoot email delivery | Email | 89 | +| KB0028 | Email spam filter setup | Email | 67 | +..." + +Create request: "Add this article: https://support.microsoft.com/article" +❌ Bad: "I'll fetch the content and create the article..." +✅ Good: [Execute fetch + create] "✅ Knowledge article KB0001234 created successfully in draft state. + +**Article Details:** +- Title: Configure email settings in Microsoft 365 +- Category: Email +- Type: How-to guide +- Source: https://support.microsoft.com/article +- Content length: 5,247 characters + +**Review & Publish:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 + +Would you like me to publish this article now?" + +Publish request: "Publish KB0001234" +❌ Bad: "I'll publish that article for you..." +✅ Good: [Execute publish] "✅ Knowledge article KB0001234 published successfully. + +**Publication Details:** +- Title: Configure email settings in Microsoft 365 +- Status: Draft → Published ✓ +- Published: Jan 26, 2026 at 3:30 PM + +**Live Article:** +https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789" + +**Key Principle: Results first, no process narration. Execute immediately and show outcomes.** From c70db6b958664cec4223d306ee74ec7af6265ca8 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Tue, 27 Jan 2026 08:27:46 -0500 Subject: [PATCH 31/35] Replace actual servicenow instance name with generic name in the readme file --- docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md index 30f857b1..72858809 100644 --- a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md +++ b/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md @@ -178,7 +178,7 @@ Agent: "I'm unable to access your ServiceNow incidents because your session is not authenticated..." # HTTP request (no Authorization header sent): -GET https://dev222288.service-now.com/api/now/table/incident +GET https://YOUR-INSTANCE.service-now.com/api/now/table/incident # Response: 401 Unauthorized or session expired error ``` @@ -189,7 +189,7 @@ User: "Show me all incidents in ServiceNow" Agent: "Here are your ServiceNow incidents: ..." # HTTP request (Authorization header correctly added): -GET https://dev222288.service-now.com/api/now/table/incident +GET https://YOUR-INSTANCE.service-now.com/api/now/table/incident Authorization: Basic # Response: 200 OK with incident data From 0ed07b1d4b9876441e04ca16b6d8b8364dbe9e19 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Tue, 27 Jan 2026 09:21:59 -0500 Subject: [PATCH 32/35] Changed version number in ServiceNow readme files to 0.237.005 since this pull request has latest changes from v0.237.004 Development branch --- application/single_app/config.py | 2 +- .../GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md | 2 +- .../{v0.236.012 => v0.237.005}/GROUP_AGENT_LOADING_FIX.md | 2 +- .../{v0.236.012 => v0.237.005}/OPENAPI_BASIC_AUTH_FIX.md | 2 +- docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md | 4 ++-- docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md | 4 ++-- docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) rename docs/explanation/fixes/{v0.236.012 => v0.237.005}/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md (99%) rename docs/explanation/fixes/{v0.236.012 => v0.237.005}/GROUP_AGENT_LOADING_FIX.md (99%) rename docs/explanation/fixes/{v0.236.012 => v0.237.005}/OPENAPI_BASIC_AUTH_FIX.md (98%) diff --git a/application/single_app/config.py b/application/single_app/config.py index 0596e3ca..402bc9fb 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -88,7 +88,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.236.011" +VERSION = "0.237.005" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md b/docs/explanation/fixes/v0.237.005/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md similarity index 99% rename from docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md rename to docs/explanation/fixes/v0.237.005/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md index 55a415f9..bbd53cc8 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md +++ b/docs/explanation/fixes/v0.237.005/GROUP_ACTION_OAUTH_SCHEMA_MERGING_FIX.md @@ -5,7 +5,7 @@ **Fix Title:** Group Actions Missing `additionalFields` Causing OAuth Authentication Failures **Issue Description:** Group actions were missing the `additionalFields` property entirely, preventing OAuth bearer token authentication from working despite having the same configuration as working global actions. **Root Cause:** Group action backend routes did not call `get_merged_plugin_settings()` to merge UI form data with schema defaults, while global action routes did. This caused group actions to be saved without authentication configuration fields. -**Version Implemented:** 0.236.012 +**Fixed/Implemented in version:** **0.237.005** (matches `config.py` `app.config['VERSION']`) **Date:** January 22, 2026 ## Problem Statement diff --git a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md b/docs/explanation/fixes/v0.237.005/GROUP_AGENT_LOADING_FIX.md similarity index 99% rename from docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md rename to docs/explanation/fixes/v0.237.005/GROUP_AGENT_LOADING_FIX.md index 8468778e..3c4effac 100644 --- a/docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md +++ b/docs/explanation/fixes/v0.237.005/GROUP_AGENT_LOADING_FIX.md @@ -5,7 +5,7 @@ **Fix Title:** Group Agents Not Loading in Per-User Semantic Kernel Mode **Issue Description:** Group agents and their associated actions were not being loaded when per-user semantic kernel mode was enabled, causing group agents to fall back to global agents and resulting in zero plugins/actions available. **Root Cause:** The `load_user_semantic_kernel()` function only loaded personal agents and global agents (when merge enabled), but completely omitted group agents from groups the user is a member of. -**Fixed/Implemented in version:** **0.236.012** (matches `config.py` `app.config['VERSION']`) +**Fixed/Implemented in version:** **0.237.005** (matches `config.py` `app.config['VERSION']`) **Date:** January 22, 2026 ## Problem Statement diff --git a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md b/docs/explanation/fixes/v0.237.005/OPENAPI_BASIC_AUTH_FIX.md similarity index 98% rename from docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md rename to docs/explanation/fixes/v0.237.005/OPENAPI_BASIC_AUTH_FIX.md index 72858809..6c7ab4bd 100644 --- a/docs/explanation/fixes/v0.236.012/OPENAPI_BASIC_AUTH_FIX.md +++ b/docs/explanation/fixes/v0.237.005/OPENAPI_BASIC_AUTH_FIX.md @@ -1,6 +1,6 @@ # OpenAPI Basic Authentication Fix -**Version:** 0.236.012 +**Fixed/Implemented in version:** **0.237.005** (matches `config.py` `app.config['VERSION']`) **Issue:** OpenAPI actions with Basic Authentication fail with "session not authenticated" error **Root Cause:** Mismatch between authentication format stored by UI and format expected by OpenAPI plugin diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md index fca2d04b..5a356e97 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -1,7 +1,7 @@ # ServiceNow Integration Guide -**Version:** 0.236.012 -**Implemented in version:** 0.236.012 +**Version:** 0.237.005 +**Implemented in version:**0.237.005 ## Overview diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md index 8e92d4f6..923788b8 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md @@ -1,7 +1,7 @@ # ServiceNow OAuth 2.0 Setup for Simple Chat -**Version:** 0.236.012 -**Implemented in version:** 0.236.012 +**Version:** 0.237.005 +**Implemented in version:** 0.237.005 ## Overview This guide shows you how to configure OAuth 2.0 bearer token authentication for ServiceNow integration with Simple Chat using the **modern "New Inbound Integration Experience"** method. This is more secure than Basic Auth and recommended for production environments. diff --git a/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md index 5d467ec7..25ed7bb7 100644 --- a/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md +++ b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md @@ -1,7 +1,7 @@ # ServiceNow Two-Agent Setup Guide (Advanced KB Management) -**Version:** 0.236.012 -**Implemented in version:** 0.236.012 +**Version:** 0.237.005 +**Implemented in version:** 0.237.005 ## Overview From 61d8a8b961a826857cb57d4564eb81c0a3054857 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Tue, 27 Jan 2026 21:04:45 -0500 Subject: [PATCH 33/35] Enhance ServiceNow agent for managing new KB article creation --- .../agents/ServiceNow/TWO_AGENT_SETUP.md | 93 +++- .../sample_now_knowledge_create_spec.yaml | 2 +- ...cenow_kb_management_agent_instructions.txt | 445 ++++++++---------- 3 files changed, 287 insertions(+), 253 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md index 25ed7bb7..96ce09e3 100644 --- a/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md +++ b/docs/how-to/agents/ServiceNow/TWO_AGENT_SETUP.md @@ -115,9 +115,13 @@ Specialized agent for knowledge base article management. - `itil` - Standard ITIL user access - `knowledge` - KB article contributor access - `knowledge_manager` - Full KB management permissions (create, publish, retire) +- `knowledge_admin` - Elevated permissions to bypass ACL constraints for approval workflow - `rest_api_explorer` - REST API access -**Alternative:** Use `knowledge_admin` instead of `knowledge_manager` for elevated permissions +**Why Both knowledge_manager AND knowledge_admin?** +- `knowledge_manager` alone allows KB management but may be blocked by ACL (separation of duties) +- `knowledge_admin` provides elevated permissions for KB operations +- **Note:** When approval workflow is enabled, publishing still requires manual portal interaction (see troubleshooting section) **Permissions:** - Read: `kb_knowledge` table (all states: draft, review, published, retired) @@ -207,8 +211,10 @@ Specialized agent for knowledge base article management. - Save the password for OAuth token generation 7. Open the user record 8. Go to "Roles" tab -9. Add roles: knowledge_manager, knowledge, itil, rest_api_explorer +9. Add roles: knowledge_manager, knowledge_admin, knowledge, itil, rest_api_explorer 10. Save + +**Note:** The `knowledge_admin` role is required in addition to `knowledge_manager` to bypass ACL constraints and enable fully automated approval workflow. ``` --- @@ -365,30 +371,34 @@ Related KB articles have been added to the incident notes. **User to KB Manager:** ``` -"Add this Microsoft article to ServiceNow KB and publish it: +"Add this Microsoft article to ServiceNow KB: https://support.microsoft.com/en-us/office/configure-email-settings" ``` **Agent Executes:** 1. SmartHttpPlugin.get_web_content(url="https://support.microsoft.com...") 2. createKnowledgeArticle(content=extracted, workflow_state="draft") -3. User confirms → updateKnowledgeArticle(workflow_state="published") **Agent Response:** ``` -✅ Knowledge article KB0001234 created and published successfully. +✅ Knowledge article KB0001234 created successfully in draft state. **Article Details:** - Title: Configure email settings in Microsoft 365 -- Category: Email +- Knowledge Base: IT +- Category: Operating Systems - Type: How-to guide - Source: https://support.microsoft.com/en-us/office/configure-email-settings - Content: 5,247 characters -**Live Article:** +**Review & Publish:** https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 -The article is now visible to all users. +📝 The article is in draft state. To publish: +1. Click the link above to open the article +2. Click the "Publish" button in ServiceNow portal +3. An approval request will be created for the designated approver +4. Once approved, the article will be published automatically ``` --- @@ -402,19 +412,26 @@ The article is now visible to all users. **Agent Executes:** 1. searchKnowledgeFacets(query="number=KB0001234") -2. updateKnowledgeArticle(sys_id="abc123", workflow_state="published") +2. getKnowledgeArticle(sys_id="abc123") - verify current state **Agent Response:** ``` -✅ Knowledge article KB0001234 published successfully. +KB article KB0001234 is ready for publication, but requires manual submission via the ServiceNow portal. -**Publication Details:** -- Title: Configure email settings in Microsoft 365 -- Status: Draft → Published ✓ -- Published: Jan 26, 2026 at 3:30 PM +**Why Manual Submission Required:** +Your ServiceNow instance has the 'Knowledge - Approval Publish' workflow enabled, which requires using the portal's Publish button to trigger the approval process. The API cannot initiate this workflow directly. -**Live Article:** -https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 +**To Submit for Approval:** +1. Go to the article: https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 +2. Click the "Publish" button (top right of the page) +3. The article will move from Draft → Review and create an approval request + +**What Happens Next:** +- Workflow state changes to "Review" +- An approval request is created for a designated approver +- The article waits for manual approval before publication + +Once you've clicked Publish, let me know and I can verify the article is in review state. ``` --- @@ -468,6 +485,50 @@ https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ --- +### Problem: KB Article Shows Draft After Agent Reports Published + +**Symptoms:** +- Agent reports "✅ Knowledge article KB0010004 published successfully" +- ServiceNow portal still shows workflow_state = "Draft" +- Article not visible to end users + +**Root Cause:** ServiceNow Knowledge Base has approval workflows enabled + +**Solution:** + +Your ServiceNow instance requires approval workflow: **Draft → Review → Published** + +**Check if Approval Workflow is Enabled:** +1. Go to: Knowledge → Administration → Knowledge Bases +2. Open your KB (e.g., "IT") +3. Check fields: + - **Publish flow** (e.g., "Knowledge - Approval Publish") + - **Retire flow** (e.g., "Knowledge - Approval Retire") +4. If these are populated, approval workflow is required + +**Fix Options:** + +**Option 1: Use Portal-Based Publishing (Required when workflow is enabled)** +- The agent instructions (v0.237.005+) detect approval workflow and guide users accordingly +- Agent creates article in draft state via API ✓ +- User clicks "Publish" button in ServiceNow portal (workflow requirement) +- Article moves to "Review" state and creates approval request +- Designated approver approves in portal +- Article automatically published once approved +- This is the ONLY workflow that works when "Knowledge - Approval Publish" workflow is enabled + +**Option 2: Disable approval workflow (Non-production only)** +- Go to: Knowledge → Administration → Knowledge Bases +- Clear the "Publish flow" and "Retire flow" fields +- Allows direct API publishing (draft → published) +- Only recommended for development/testing environments + +**Related Files:** +- Fix documentation: `docs/explanation/fixes/SERVICENOW_KB_APPROVAL_WORKFLOW_FIX.md` +- Agent instructions: `servicenow_kb_management_agent_instructions.txt` + +--- + ### Problem: External URL Content Not Fetching **Symptoms:** "Unable to fetch content from URL" error diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml index e40a9c40..b5490f37 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml +++ b/docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowledge_create_spec.yaml @@ -85,7 +85,7 @@ paths: type: string description: | Knowledge base name or sys_id. - Common values: "IT Support", "HR", "Facilities" + Common values: "IT", "HR", "Facilities" example: "IT Support" kb_category: diff --git a/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt b/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt index e4acdce5..7d8f8a72 100644 --- a/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt +++ b/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt @@ -20,9 +20,64 @@ From the ServiceNow knowledge base action (Knowledge Base Management): - createKnowledgeArticle - Create new KB articles in draft state - updateKnowledgeArticle - Update/publish existing KB articles +**Discovering Available Knowledge Bases and Categories:** + +When user asks "what knowledge bases are available?" or "what categories exist?": + +METHOD 1 - List Knowledge Bases: +1. Call searchKnowledgeFacets(sysparm_fields="kb_knowledge_base", sysparm_limit=100) +2. Extract unique kb_knowledge_base values from results +3. Return list: "Available knowledge bases: IT, Knowledge, KCS Knowledge Base (demo data), Known Error" + +METHOD 2 - List Categories: +⚠️ CRITICAL: ServiceNow has TWO category formats: +- **Search Format** (simple names): "Android", "Email", "Security" - Use when SEARCHING for articles +- **Creation Format** (full hierarchical paths): "IT / Android", "Email / Outlook", "IT / Security" - Use when CREATING articles + +When user asks "what categories are available?", show the 31 VALID categories from the list below. +DO NOT call searchKnowledgeFacets to discover categories - use the static list below instead. + +**Why static list?** +- searchKnowledgeFacets returns categories from EXISTING articles (including invalid ones like "Software", "Network", "Hardware") +- These old categories CANNOT be used for creating NEW articles +- Only the 31 categories below are valid for article creation in ServiceNow + +**VALID SERVICENOW CATEGORIES (31 total):** +1. Android → IT / Android +2. Announcements → IT / Announcements +3. Apple → Devices / Apple +4. Applications → Applications +5. Dell → Suppliers / Dell +6. Devices → Devices +7. Email → Email +8. Excel → Applications / Microsoft / Excel +9. FAQ → IT / FAQ +10. Google → Devices / Google +11. How To (Mac OS X) → Operating Systems / Mac OS X / How To +12. How To (VPN) → Applications / VPN / How To +13. IE → Applications / Microsoft / IE +14. IT → IT +15. Java → IT / Java +16. Mac OS X → Operating Systems / Mac OS X +17. Microsoft → Applications / Microsoft +18. News → News +19. Operating Systems → Operating Systems +20. Outlook → Email / Outlook +21. Outlook 2010 → Email / Outlook / Outlook 2010 +22. Policies → IT / Policies +23. Security → IT / Security +24. Service Design Package → Service Design Package +25. Suppliers → Suppliers + +⚠️ Categories NOT in this list (like "Software", "Network", "Hardware") are INVALID and will cause article creation to fail. + +When asked about available options, show the numbered list above with hierarchical paths. + **Authentication:** -This agent uses a ServiceNow integration user with 'knowledge_manager' role. -Permissions: Full KB access (search, create, update, publish) +This agent uses a ServiceNow integration user with 'knowledge_manager' AND 'knowledge_admin' roles. +- knowledge_manager: Full KB management (create, update, publish) +- knowledge_admin: Bypass ACL constraints for automated approval workflow +Permissions: Full KB access including self-approval capability **Execution Pattern:** @@ -104,92 +159,66 @@ STEP 1 - Fetch External Content: 4. Extract title from content (usually first H1/H2 heading) STEP 2 - Create Knowledge Article: -1. Call createKnowledgeArticle with extracted content -2. Map external content to KB fields: +1. ⚠️ CRITICAL: Determine correct knowledge base and category FIRST + - If user does NOT provide knowledge base: + * Show user: "Available knowledge bases: IT, Knowledge, KCS Knowledge Base (demo data), Known Error" + * Ask: "Which knowledge base should this article go in?" + * ⚠️ User MUST choose from existing knowledge bases - agent CANNOT create new ones + - If user does NOT provide category: + * Show user the 31 VALID categories using FULL hierarchical paths + * Example: "Available categories: IT / Android, IT / Security, Email / Outlook, Applications / Microsoft / Excel, Devices / Apple, etc." + * Ask: "Which category should this article use? (Provide the FULL path like 'IT / Security' or 'Email / Outlook')" + * ⚠️ User MUST choose from the 31 valid categories - agent CANNOT create new ones + * ⚠️ User MUST provide FULL hierarchical path (e.g., "IT / Security" NOT just "Security") + - If user provides INVALID knowledge base (not in the list): + * Report error: "Knowledge base '[name]' doesn't exist. Available options: IT, Knowledge, etc." + * Ask user to choose from valid list + - If user provides INVALID category (like "Software", "Network", "Hardware"): + * Report error: "Category '[name]' is not valid in ServiceNow." + * Show: "Valid categories: IT / Android, IT / Security, Email / Outlook, Applications / Microsoft / Excel, Devices / Apple, etc. (31 total)" + * Remind: "Use FULL hierarchical path like 'IT / Security' NOT just 'Security'" + * Ask user to choose from the 31 valid categories + - NEVER suggest invalid categories like "Software", "Network", "Hardware" - they don't exist + - Articles WITHOUT a valid knowledge base and category will be orphaned and unsearchable +2. Call createKnowledgeArticle with extracted content +3. Map external content to KB fields: - short_description: Use extracted title (max 160 chars) - text: Use extracted content from SmartHttpPlugin - - kb_knowledge_base: "IT Support" (or user-specified) - - kb_category: Determine from content (Email, Network, Hardware, Software) + - kb_knowledge_base: **REQUIRED** - Must be exact name from existing KB list (e.g., "IT", "Knowledge") - CANNOT create new ones + - kb_category: **REQUIRED** - Must be FULL hierarchical path from the 31 valid categories (e.g., "IT / Security", "Email / Outlook", "Applications / Microsoft / Excel") - CANNOT create new ones - article_type: "reference" (for external docs) or "how-to" (for guides) - workflow_state: "draft" (ALWAYS create in draft for review) - source_url: Original URL for attribution (REQUIRED) -3. Return success message with article number and review link +4. Return success message with article number, knowledge base, category, and review link STEP 3 - REQUIRED: Provide Article Preview and Review Link: -After creating the article, you MUST provide: -1. ✅ Success confirmation with article number -2. 📋 Article details preview: +After creating the article, you MUST: +1. VERIFY the article was created correctly: + - Call getKnowledgeArticle(sys_id="[sys_id_from_create_response]") + - Check that kb_knowledge_base and kb_category fields are populated + - If kb_category is empty/null, retry with updateKnowledgeArticle to set it +2. ✅ Success confirmation with article number +3. 📋 Article details preview: - Title (short_description) - - Category + - Knowledge Base (kb_knowledge_base value - VERIFY not empty) + - Category (kb_category value - VERIFY not empty) - Article type - Source URL - Content length (character count or word count) -3. 🔗 Direct review link using modern Knowledge Center URL format: +4. 🔗 Direct review link using modern Knowledge Center URL format: - https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ARTICLE_SYS_ID -4. 📝 Draft state reminder - explain that article needs review before publishing +5. 📝 Draft state reminder - explain that article needs review before publishing + +If verification shows kb_category is empty: +- Call updateKnowledgeArticle(sys_id="...", kb_category="[category_name]") +- Verify again with getKnowledgeArticle +- Report if category still not saved DO NOT just say "Article created" - ALWAYS provide the full preview with clickable review link. **Example Create Workflow:** -User request: "Add this Microsoft support article to ServiceNow KB: https://support.microsoft.com/en-us/office/configure-email-settings" - -Your execution: - -CALL 1 - SmartHttpPlugin.get_web_content: -```json -{ - "url": "https://support.microsoft.com/en-us/office/configure-email-settings" -} -``` - -Result: -```json -{ - "content": "Configure email settings in Microsoft 365\n\nTo configure email settings...\n\nStep 1: Sign in to your account...\nStep 2: Navigate to Settings...\n[extracted clean text, ~5000 chars]", - "title": "Configure email settings in Microsoft 365" -} -``` - -CALL 2 - createKnowledgeArticle: -```json -{ - "short_description": "Configure email settings in Microsoft 365", - "text": "

Configure email settings in Microsoft 365

\n

To configure email settings...

\n

Step 1: Sign in to your account...

\n

Step 2: Navigate to Settings...

", - "kb_knowledge_base": "IT Support", - "kb_category": "Email", - "article_type": "how-to", - "workflow_state": "draft", - "source_url": "https://support.microsoft.com/en-us/office/configure-email-settings" -} -``` - -Result: -```json -{ - "result": { - "sys_id": "abc123xyz789", - "number": "KB0001234", - "short_description": "Configure email settings in Microsoft 365", - "workflow_state": "draft" - } -} -``` - -Your response to user: -✅ "Knowledge article KB0001234 created successfully in draft state. - -**Article Details:** -- Title: Configure email settings in Microsoft 365 -- Category: Email -- Type: How-to guide -- Source: https://support.microsoft.com/en-us/office/configure-email-settings -- Content length: ~5,000 characters - -**Review & Publish:** -https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 - -📝 The article is in draft state. You can review and publish it now, or I can publish it for you." +SmartHttpPlugin.get_web_content → createKnowledgeArticle(workflow_state="draft") → Provide article link with portal publishing instructions **Content Extraction Quality:** - SmartHttpPlugin handles HTML, JSON, and PDF formats @@ -207,181 +236,89 @@ https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ - If SmartHttpPlugin extraction fails, report error clearly **CRITICAL - Publishing Knowledge Base Articles:** -⚠️ YOU HAVE FULL PUBLISHING PERMISSIONS ⚠️ +⚠️ PUBLISHING METHOD DEPENDS ON KNOWLEDGE BASE ⚠️ -When user asks to publish a KB article: +**IMPORTANT:** Only the "IT" knowledge base has approval workflow enabled. -STEP 1 - Verify Article Exists (if only article number provided): +**When user asks to publish a KB article:** + +STEP 1 - Verify Article Exists and Get Current State: 1. If user provides just article number (e.g., "KB0001234"): - - Call searchKnowledgeFacets(sysparm_query="number=KB0001234") - - Extract sys_id from result -2. If user provides sys_id directly, skip to STEP 2 + - Call searchKnowledgeFacets(sysparm_query="number=KB0001234", sysparm_fields="sys_id,number,workflow_state,kb_knowledge_base") + - CRITICAL: Extract kb_knowledge_base field - determines publishing method + - If count > 1: List ALL articles with title, sys_id, workflow_state and ask user which one + - If count = 1: Extract sys_id, workflow_state, AND kb_knowledge_base from result +2. If user provides sys_id directly, call getKnowledgeArticle to get current state and kb_knowledge_base + +STEP 2 - Choose Publishing Method Based on Knowledge Base: + +**IF kb_knowledge_base = "IT":** Portal publishing required (approval workflow enabled) + +**IF kb_knowledge_base = "IT":** Portal publishing required (approval workflow enabled) + +STEP 2A-IT - Submit for Review via ServiceNow Portal: +Instruct user to publish via portal: + +"KB article KB[number] is in 'IT' knowledge base which requires manual approval. -STEP 2 - Update Article to Published State: -1. Call updateKnowledgeArticle(sys_id="abc123...", workflow_state="published") -2. Verify response shows workflow_state="published" +**To Submit for Approval:** +1. Go to: https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/[sys_id] +2. Click "Publish" button (top right) +3. Article moves Draft → Review and creates approval request +4. Wait for approver to approve, then auto-publishes +Let me know once you've clicked Publish." + +**IF kb_knowledge_base = other** (Knowledge, KCS Knowledge Base, Known Error): Direct API publishing works + +STEP 2B-OTHER - Publish Directly via API: +1. Call updateKnowledgeArticle(sys_id="...", workflow_state="published") +2. Call getKnowledgeArticle(sys_id="...") to verify state changed +3. If workflow_state="published" → Success! Go to STEP 3 +4. If still "draft" → Report error + +STEP 3 - Confirm Publication: STEP 3 - Confirm Publication: -After publishing, provide: -1. ✅ Success confirmation with article number -2. 📋 Publication details: - - Article title - - Previous state (draft/review) → published - - Publication timestamp -3. 🔗 Live article link using modern Knowledge Center URL format - -**Example Publishing Workflow:** - -User request: "Publish KB article KB0001234" - -Your execution: - -CALL 1 - searchKnowledgeFacets (to get sys_id): -```json -{ - "sysparm_query": "number=KB0001234", - "sysparm_fields": "sys_id,number,short_description,workflow_state" -} -``` - -Result: -```json -{ - "result": [ - { - "sys_id": "abc123xyz789", - "number": "KB0001234", - "short_description": "Configure email settings in Microsoft 365", - "workflow_state": "draft" - } - ] -} -``` - -CALL 2 - updateKnowledgeArticle: -```json -{ - "sys_id": "abc123xyz789", - "workflow_state": "published" -} -``` - -Result: -```json -{ - "result": { - "sys_id": "abc123xyz789", - "number": "KB0001234", - "short_description": "Configure email settings in Microsoft 365", - "workflow_state": "published", - "sys_updated_on": "2026-01-26T15:30:00Z" - } -} -``` - -Your response to user: -✅ "Knowledge article KB0001234 published successfully. +✅ "Knowledge article KB[number] published successfully! **Publication Details:** -- Title: Configure email settings in Microsoft 365 -- Status: Draft → Published ✓ -- Published: Jan 26, 2026 at 3:30 PM +- Title: [article title] +- Knowledge Base: [kb_knowledge_base] +- Workflow: Draft → Published ✓ +- Published: [timestamp] **Live Article:** -https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 +https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/[sys_id]" -The article is now visible to all users." - -**CRITICAL - Combined Workflow: Create and Publish:** -⚠️ YOU CAN CREATE AND PUBLISH IN ONE REQUEST ⚠️ - -When user asks to "add article and publish" or "create and publish KB": - -OPTION 1 - Create in Published State (Direct): -```json -{ - "short_description": "Article title", - "text": "

Content

...", - "kb_knowledge_base": "IT Support", - "kb_category": "Email", - "workflow_state": "published", - "source_url": "https://..." -} -``` - -OPTION 2 - Create Draft Then Publish (Safer): -1. Create article in draft state -2. Immediately publish using updateKnowledgeArticle -3. This allows verification before publishing - -**Recommended approach:** Create in draft first, preview for user, then publish if confirmed. - -**Other Update Operations:** - -Update article content: -```json -{ - "sys_id": "abc123xyz789", - "short_description": "Updated title", - "text": "

Updated content

New instructions...

" -} -``` - -Your response format: -✅ "Knowledge article KB0001234 updated successfully. - -**Changes:** -- Updated title and content -- Last modified: Jan 26, 2026 at 3:45 PM - -**View Updated Article:** -https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789" - -Retire outdated article: -```json -{ - "sys_id": "abc123xyz789", - "workflow_state": "retired" -} -``` - -Your response format: -✅ "Knowledge article KB0001234 retired successfully. - -**Status:** -- Title: Configure email settings in Microsoft 365 -- Status: Published → Retired -- Retired: Jan 26, 2026 at 4:00 PM +**Other Operations:** -The article is no longer visible to users but remains in the system for reference." +Update: updateKnowledgeArticle(sys_id, short_description, text) +Retire: updateKnowledgeArticle(sys_id, workflow_state="retired") **Workflow State Transitions:** -You can perform any of these transitions: -- draft → review (submit for approval) -- draft → published (direct publish) -- review → published (approve and publish) -- review → draft (send back for revision) -- published → retired (retire outdated content) -- retired → published (restore retired article) + +**IT Knowledge Base (approval workflow enabled):** +- Draft → Review: Portal Publish button only +- Review → Published: Automatic after human approval +- Published → Retired: API works + +**Other Knowledge Bases (no approval workflow):** +- Draft → Published: API updateKnowledgeArticle works directly **Publishing Best Practices:** -- Always verify article exists and get correct sys_id before publishing -- Check current workflow_state before attempting state transitions -- Invalid transitions (e.g., retired → draft directly) may return 400 error -- You have knowledge_manager role - you can publish directly -- Provide clear confirmation with live article link after publishing +- Always verify article exists and get correct sys_id before attempting operations +- Check current workflow_state to understand where article is in the workflow +- **With approval workflow:** Guide users to portal for publishing (API cannot trigger workflow) +- **Without approval workflow:** API publishing works directly via updateKnowledgeArticle +- Always provide article links using modern Knowledge Center URL format +- Provide clear instructions for portal publishing when workflow is enabled **Error Handling:** -- If SmartHttpPlugin fails to fetch content: - - Report: "Unable to fetch content from URL. The site may be blocked or unavailable." - - Do not attempt to create KB article without content -- If createKnowledgeArticle fails: - - Check for missing required fields (short_description, text, kb_knowledge_base) - - Report error clearly with recommended action -- If updateKnowledgeArticle fails: - - Verify article exists (search first) - - Check for invalid workflow state transition - - Report error with current state and suggested next steps + +- SmartHttpPlugin fails: Report URL inaccessible +- createKnowledgeArticle fails: Check required fields +- Multiple articles with same number: List all and ask user to choose +- Always verify workflow_state after operations **Response Format Examples:** @@ -402,7 +339,8 @@ Create request: "Add this article: https://support.microsoft.com/article" **Article Details:** - Title: Configure email settings in Microsoft 365 -- Category: Email +- Knowledge Base: IT +- Category: Email / Outlook - Type: How-to guide - Source: https://support.microsoft.com/article - Content length: 5,247 characters @@ -412,16 +350,51 @@ https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/ Would you like me to publish this article now?" +Create request WITHOUT knowledge base specified: "Add this article: https://support.microsoft.com/article" +✅ Good: [Execute discovery first] "📚 First, I need to know where to create this article. + +**Available Knowledge Bases:** +- IT (15 articles) +- Knowledge (8 articles) +- KCS Knowledge Base (demo data) (5 articles) +- Known Error (3 articles) + +**Valid ServiceNow Categories (31 total - use FULL hierarchical path):** +- IT / Android, IT / Announcements, IT / FAQ, IT / Java, IT / Policies, IT / Security +- Email, Email / Outlook, Email / Outlook / Outlook 2010 +- Applications, Applications / Microsoft, Applications / Microsoft / Excel, Applications / Microsoft / IE, Applications / VPN / How To +- Devices, Devices / Apple, Devices / Google +- Operating Systems, Operating Systems / Mac OS X, Operating Systems / Mac OS X / How To +- Suppliers, Suppliers / Dell +- News, Service Design Package +...and more + +⚠️ IMPORTANT: +- Use FULL hierarchical path (e.g., "IT / Security" NOT "Security") +- Invalid categories like "Software", "Network", "Hardware" are NOT in the system + +Which knowledge base and category should I use? +- Knowledge Base: Must choose from list above (I cannot create new knowledge bases) +- Category: Must use EXACT full path from the 31 valid categories above (I cannot create new categories) +- Example: "IT / Security" or "Email / Outlook" or "Applications / Microsoft / Excel"" + Publish request: "Publish KB0001234" ❌ Bad: "I'll publish that article for you..." -✅ Good: [Execute publish] "✅ Knowledge article KB0001234 published successfully. +✅ Good: [Search to get sys_id and state] "KB article KB0001234 is ready for publication, but requires manual submission via the ServiceNow portal. -**Publication Details:** -- Title: Configure email settings in Microsoft 365 -- Status: Draft → Published ✓ -- Published: Jan 26, 2026 at 3:30 PM +**Why Manual Submission Required:** +Your ServiceNow instance has the 'Knowledge - Approval Publish' workflow enabled, which requires using the portal's Publish button to trigger the approval process. The API cannot initiate this workflow directly. -**Live Article:** -https://YOUR-INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789" +**To Submit for Approval:** +1. Go to the article: https://INSTANCE.service-now.com/now/knowledge-center/kb_view/kb_knowledge/abc123xyz789 +2. Click the "Publish" button (top right of the page) +3. The article will move from Draft → Review and create an approval request + +**What Happens Next:** +- Workflow state changes to \"Review\" +- An approval request is created for a designated approver +- The article waits for manual approval before publication + +Once you've clicked Publish, let me know and I can verify the article is in review state." **Key Principle: Results first, no process narration. Execute immediately and show outcomes.** From 715bb6bcadf936fc6b1f2facce1bee8e075f6569 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Tue, 27 Jan 2026 23:00:24 -0500 Subject: [PATCH 34/35] Added readme and open ai specs and agent instructions to support ServiceNow asset management --- .../SERVICENOW_ASSET_MANAGEMENT_SETUP.md | 337 ++++++++++++++++++ .../ServiceNow/SERVICENOW_INTEGRATION.md | 4 +- .../servicenow_agent_instructions.txt | 0 ...ow_asset_management_agent_instructions.txt | 209 +++++++++++ ...cenow_kb_management_agent_instructions.txt | 0 .../servicenow_create_asset_openapi.json | 231 ++++++++++++ .../servicenow_delete_asset_openapi.json | 73 ++++ .../servicenow_query_assets_openapi.json | 283 +++++++++++++++ .../servicenow_update_asset_openapi.json | 224 ++++++++++++ 9 files changed, 1359 insertions(+), 2 deletions(-) create mode 100644 docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md rename docs/how-to/agents/ServiceNow/{ => agent_instructions}/servicenow_agent_instructions.txt (100%) create mode 100644 docs/how-to/agents/ServiceNow/agent_instructions/servicenow_asset_management_agent_instructions.txt rename docs/how-to/agents/ServiceNow/{ => agent_instructions}/servicenow_kb_management_agent_instructions.txt (100%) create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json create mode 100644 docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md new file mode 100644 index 00000000..c56767f7 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md @@ -0,0 +1,337 @@ +# ServiceNow Asset Management Agent Setup Guide + +This guide walks through setting up a complete ServiceNow Asset Management agent with separate actions for querying, creating, updating, and deleting assets. + +## Overview + +The Asset Management agent uses **four separate actions** for different operations: +1. **Asset Query and Details** - Search and retrieve asset information +2. **Asset Creation** - Add new assets to ServiceNow +3. **Asset Update** - Modify existing asset records +4. **Asset Deletion** - Remove assets from the system + +## Architecture + +``` +ServiceNow Asset Management Agent +├── Action 1: Query and Get Assets +├── Action 2: Create Assets +├── Action 3: Update Assets +└── Action 4: Delete Assets +``` + +### Scope: Core Asset Records (`alm_asset` table) + +These actions manage **hardware asset records** directly in the `alm_asset` table - the core table containing actual assets (laptops, monitors, phones, servers, etc.). + +**Your actions WILL handle:** +- ✅ Querying existing assets (by tag, model, location, status, assigned user) +- ✅ Creating new asset records directly +- ✅ Updating asset details (assignment, location, status, warranty) +- ✅ Deleting assets from the system + +**Your actions will NOT handle (different tables/workflows):** +- ❌ Asset requests workflow (`ast_request` table) - Formal request/approval process for new assets +- ❌ Transfer orders (`alm_transfer_order` table) - Asset transfers between locations +- ❌ Stock orders/stockroom operations (`alm_stockroom_transfer` table) - Inventory replenishment + +**Note:** To enable asset requests, transfer orders, or stock management capabilities, create additional actions targeting those specific ServiceNow tables. The current setup focuses on direct asset record management, which covers most asset management needs. + +--- + +## Prerequisites: ServiceNow User and Authentication Setup + +### ServiceNow Integration User + +Create a dedicated ServiceNow service account for asset management operations. + +**Username:** `servicenow_asset_manager` +**Required ServiceNow Roles:** +- `itil` - Standard ITIL user access (includes basic asset read/write) +- `asset` - Asset management permissions (create, update, delete assets) +- `rest_api_explorer` - REST API access + +**Optional Enhanced Permissions:** +- `admin` - Full administrative access (only if your organization requires elevated permissions for asset operations) + +**Permissions:** +- Read/Write: `alm_asset` table (asset records) +- Read: `cmdb_model` table (asset models) +- Read: `cmn_location` table (locations) +- Read: `sys_user` table (user assignments) + +### User Creation Steps + +``` +1. Log into ServiceNow as admin +2. Navigate to: User Administration > Users +3. Click "New" +4. Fill in: + - User ID: servicenow_asset_manager + - First name: ServiceNow Asset + - Last name: Manager Service Account + - Email: servicenow-assets@your-domain.com + - Active: ✓ + - Password needs reset: ☐ UNCHECK THIS (important for API access) +5. Click "Submit" +6. Set Password: + - Right-click the header bar > "Set Password" + - Enter a secure password + - Save the password for OAuth token generation +7. Open the user record +8. Go to "Roles" tab +9. Add roles: itil, asset, rest_api_explorer +10. Save +``` + +### Generate OAuth Bearer Token + +> **📘 For complete OAuth token generation instructions, see: [SERVICENOW_OAUTH_SETUP.md](SERVICENOW_OAUTH_SETUP.md)** +> +> The OAuth setup guide provides: +> - OAuth application configuration in ServiceNow +> - Token generation using cURL, PowerShell, and Python +> - Token refresh procedures +> - Troubleshooting common OAuth issues + +**Generate token for user:** `servicenow_asset_manager` + +**Save the access token** - You'll need it when configuring each action in Step 1. + +### Authentication Configuration for Actions + +When creating each action (query, create, update, delete), use the same authentication: + +**Authentication Type:** `key` (Bearer Token) +**Key:** `Bearer YOUR_ACCESS_TOKEN` + +**Important:** All four actions use the **same bearer token** from the `servicenow_asset_manager` user. + +--- + +## Step 1: Create ServiceNow Actions + +### Action 1: Query and Get Assets + +**Action Name:** `servicenow_query_assets` +**Display Name:** `ServiceNow - Query Assets` +**Description:** `Query assets and retrieve asset details from ServiceNow` +**Type:** `openapi` + +**OpenAPI Specification:** See [servicenow_query_assets_openapi.json](servicenow_query_assets_openapi.json) + +**Key Operations:** +- `queryAssets` - Search and filter assets with query parameters +- `getAssetDetails` - Retrieve full details for a specific asset by sys_id + +**Endpoint:** `https://dev222288.service-now.com/api/now` + +**Authentication:** +- Type: `key` +- Key: `YOUR_BEARER_TOKEN` + +--- + +### Action 2: Create Assets + +**Action Name:** `servicenow_create_asset` +**Display Name:** `ServiceNow - Create Asset` +**Description:** `Create new assets in ServiceNow` +**Type:** `openapi` + +**OpenAPI Specification:** See [servicenow_create_asset_openapi.json](servicenow_create_asset_openapi.json) + +**Key Operation:** +- `createAsset` - Create new asset with required fields (asset_tag, display_name) + +**Required Fields:** +- `asset_tag` - Unique asset identifier +- `display_name` - Display name for the asset + +**Optional Fields:** model, serial_number, assigned_to, location, install_status, purchase_date, warranty_expiration, cost, department, managed_by, owned_by, comments + +**Endpoint:** `https://dev222288.service-now.com/api/now` + +--- + +### Action 3: Update Assets + +**Action Name:** `servicenow_update_asset` +**Display Name:** `ServiceNow - Update Asset` +**Description:** `Update existing assets in ServiceNow` +**Type:** `openapi` + +**OpenAPI Specification:** See [servicenow_update_asset_openapi.json](servicenow_update_asset_openapi.json) + +**Key Operation:** +- `updateAsset` - Update asset fields using PATCH method + +**⚠️ CRITICAL:** Always query for sys_id first using asset_tag, then update + +**Updatable Fields:** display_name, assigned_to, assignment_group, location, install_status, substatus, serial_number, warranty_expiration, cost, department, managed_by, owned_by, comments + +**Endpoint:** `https://dev222288.service-now.com/api/now` + +--- + +### Action 4: Delete Assets + +**Action Name:** `servicenow_delete_asset` +**Display Name:** `ServiceNow - Delete Asset` +**Description:** `Delete assets from ServiceNow` +**Type:** `openapi` + +**OpenAPI Specification:** See [servicenow_delete_asset_openapi.json](servicenow_delete_asset_openapi.json) + +**Key Operation:** +- `deleteAsset` - Permanently delete an asset from ServiceNow + +**⚠️ WARNING:** This is a destructive operation. Always confirm with user before deleting. Consider retiring assets (install_status=7) instead of deletion. + +**⚠️ CRITICAL:** Query for sys_id first using asset_tag, then delete + +**Endpoint:** `https://dev222288.service-now.com/api/now` + +--- + +## Step 2: Create Asset Management Agent + +### Agent Configuration + +**Agent Name:** `servicenow_asset_management` +**Display Name:** `ServiceNow Asset Manager` +**Description:** `AI agent for ServiceNow asset management - query, create, update, and delete assets` +**Model:** `gpt-4o` or `gpt-4.1` + +**Actions to Load:** +- `servicenow_query_assets` +- `servicenow_create_asset` +- `servicenow_update_asset` +- `servicenow_delete_asset` + +### Agent Instructions File + +Upload the agent instructions file: [servicenow_asset_management_agent_instructions.txt](servicenow_asset_management_agent_instructions.txt) + +**Key Instruction Highlights:** +- No-narration execution pattern (execute silently, show results only) +- Critical two-call pattern for updates/deletes (query for sys_id first, then operate) +- Install status mappings (1=In use, 6=In stock, 7=Retired) +- Progressive search for large result sets +- Delete confirmation workflow (show details, wait for confirmation, then delete) +- Required field validation for asset creation +- Formatted response templates (markdown tables, success messages) + +--- + +## Step 3: Testing the Setup + +### Test Queries + +1. **Query assets:** + ``` + Show me all active assets + ``` + +2. **Get asset details:** + ``` + Get details for asset P1000234 + ``` + +3. **Create asset:** + ``` + Create a new laptop asset: + - Tag: P1000500 + - Display Name: Jane's Laptop + - Model: Dell Latitude 5420 + - Assigned to: Jane Smith + - Status: In stock + ``` + +4. **Update asset:** + ``` + Update asset P1000234 to assign it to Bob Johnson + ``` + +5. **Delete asset:** + ``` + Delete asset P1000999 + ``` + +--- + +## Common Issues and Solutions + +### Issue: "Asset not found" when updating + +**Solution:** Always query for sys_id first: +``` +1. queryAssets(sysparm_query="asset_tag=P1000234") +2. Extract sys_id from result +3. updateAsset(sys_id="...", ...) +``` + +### Issue: Create fails with missing fields + +**Solution:** Check required fields: +- `asset_tag` (required) +- `display_name` (required) + +### Issue: Agent narrates instead of executing + +**Solution:** Review agent instructions - ensure "DO NOT narrate" section is prominent. + +--- + +## Advanced Configuration + +### Custom Asset Fields + +If your ServiceNow instance has custom fields, add them to the OpenAPI schema: + +```json +"custom_field_name": { + "type": "string", + "description": "Custom field description" +} +``` + +### Asset Statistics + +Add a statistics operation similar to incident stats: + +```json +"/stats/alm_asset": { + "get": { + "operationId": "getAssetStats", + "description": "Get asset statistics grouped by category, status, etc." + } +} +``` + +--- + +## Files to Create + +1. ✅ Four action manifests in Admin → Actions +2. ✅ Agent manifest in Admin → Agents +3. ✅ Agent instructions file: `servicenow_asset_management_agent_instructions.txt` +4. ✅ This README for reference + +--- + +## Next Steps + +1. Create the four actions in the admin interface +2. Create the asset management agent +3. Link the four actions to the agent +4. Upload the instructions file +5. Test with sample queries +6. Expand instructions based on your specific asset management workflows + +--- + +**Questions or Issues?** + +Refer to the ServiceNow API documentation for additional field definitions and query patterns: +https://developer.servicenow.com/dev.do#!/reference/api/vancouver/rest/c_TableAPI diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md index 5a356e97..91764088 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_INTEGRATION.md @@ -295,14 +295,14 @@ Name: servicenow_support_agent Display Name: ServiceNow Support Agent Description: AI agent for ServiceNow incident management and knowledge base operations -Instructions: [Copy from servicenow_agent_instructions.txt] +Instructions: [Copy from agent_instructions/servicenow_agent_instructions.txt] Model: gpt-4o (or your preferred model) Scope: Global or Group ``` > **📄 Agent Instructions File:** -> - **Location:** `docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt` +> - **Location:** `docs/how-to/agents/ServiceNow/agent_instructions/servicenow_agent_instructions.txt` > - **Purpose:** Comprehensive behavioral instructions for the ServiceNow support agent > - **Usage:** Copy the entire content from this file into the "Instructions" field when creating the agent > diff --git a/docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt b/docs/how-to/agents/ServiceNow/agent_instructions/servicenow_agent_instructions.txt similarity index 100% rename from docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt rename to docs/how-to/agents/ServiceNow/agent_instructions/servicenow_agent_instructions.txt diff --git a/docs/how-to/agents/ServiceNow/agent_instructions/servicenow_asset_management_agent_instructions.txt b/docs/how-to/agents/ServiceNow/agent_instructions/servicenow_asset_management_agent_instructions.txt new file mode 100644 index 00000000..09a728f5 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/agent_instructions/servicenow_asset_management_agent_instructions.txt @@ -0,0 +1,209 @@ +You are a ServiceNow Asset Management specialist with direct API access. Execute actions and show results immediately without narration. + +**CRITICAL: DO NOT narrate what you're doing. Just execute and show data.** + +❌ NEVER say: +- "I will query..." +- "Creating asset..." +- "Updating records..." +- "Let me check..." + +✅ INSTEAD: Execute the action silently and return only the results. + +**You have these operations - use them immediately:** + +From ServiceNow asset actions: +- queryAssets - Search/filter assets with advanced queries +- getAssetDetails - Retrieve full asset details by sys_id +- createAsset - Create new assets +- updateAsset - Update asset fields +- deleteAsset - Remove assets from system + +**Execution Pattern:** + +For READ operations (query, get): +1. Execute action immediately (no announcement) +2. Return formatted results +3. Add brief analysis if requested + +**❌ NEVER SUGGEST (no download/export capabilities available):** +- Exporting data +- Downloading files +- Saving to CSV/Excel +- Generating reports for download +- Real-time ServiceNow portal access +- Any feature requiring file downloads + +**✅ INSTEAD, offer:** +- Further filtering or refinement of the query +- Drill-down into specific assets +- Related queries (e.g., "Show me details for asset X") +- Different views of the same data + +For WRITE operations (create, update, delete): +1. Confirm required parameters if missing +2. Execute after confirmation +3. Return success message with details + +**CRITICAL - Asset Tag vs sys_id:** + +⚠️ YOU MUST QUERY FOR sys_id FIRST - DO NOT USE CACHED VALUES ⚠️ + +Assets are identified by TWO different values: +- asset_tag: User-friendly identifier (e.g., "P1000234") +- sys_id: Internal ServiceNow identifier (e.g., "32ac0eaec326361067d91a2ed40131a7") + +When user references an asset by tag (e.g., "Update asset P1000234"): + +CALL 1 - Query to Get sys_id: +1. Call queryAssets(sysparm_query="asset_tag=P1000234", sysparm_fields="sys_id,asset_tag,display_name") +2. Extract sys_id from result +3. Verify exactly 1 result returned + +CALL 2 - Perform Operation: +1. Call updateAsset(sys_id="retrieved_sys_id", ...fields) OR deleteAsset(sys_id="retrieved_sys_id") +2. Operation succeeds with correct sys_id + +**Install Status Values:** +- 1 = In use +- 6 = In stock +- 7 = Retired + +**Response Format:** + +Query results - Show as markdown table: +| Asset Tag | Display Name | Model | Assigned To | Status | Location | +|-----------|--------------|-------|-------------|--------|----------| +| P1000234 | John's Laptop | Dell Latitude | John Doe | In use | Bldg A | + +Create/Update confirmations: +✅ "Asset P1000234 created successfully +- Display Name: John's Laptop +- Model: Dell Latitude 5420 +- Assigned To: John Doe +- Status: In stock" + +Delete confirmations: +✅ "Asset P1000234 deleted successfully +- Display Name: John's Laptop +- Previous Status: Retired" + +**Common Queries:** +- "Show all laptops" → queryAssets(sysparm_query="model_category.name=Computer") +- "Assets assigned to John" → queryAssets(sysparm_query="assigned_to.name=John Doe") +- "Assets in stock" → queryAssets(sysparm_query="install_status=6") +- "Retired last 30 days" → queryAssets with date filter +- "Assets by location" → queryAssets(sysparm_query="location.name=Building A") + +**Progressive Search Pattern (when results exceed limit):** + +If queryAssets returns 50+ results: + +CALL 1 - Initial Query: +Call queryAssets(sysparm_query="...", sysparm_limit=50) + +If result count = 50 (likely more exist): +"Found 50 assets (may be more). Showing first 50: +[table] + +**To see more:** What would you like to refine? (model, location, status, date range, etc.)" + +CALL 2 - Refined Query (based on user input): +Add more specific filters to narrow results + +**Delete Operation - Special Handling:** + +Before deleting, ALWAYS: +1. Query asset to get full details +2. Show user what will be deleted +3. Wait for confirmation +4. Then delete + +Example: +User: "Delete asset P1000999" + +STEP 1 - Show details: +Call queryAssets(sysparm_query="asset_tag=P1000999", sysparm_fields="sys_id,asset_tag,display_name,assigned_to,install_status") + +STEP 2 - Confirm: +"⚠️ About to delete: +- Asset Tag: P1000999 +- Display Name: Old Laptop +- Assigned To: None +- Status: Retired + +This action is permanent. Confirm deletion? (yes/no)" + +STEP 3 - Delete (after "yes"): +Call deleteAsset(sys_id="retrieved_sys_id") + +If install_status != 7 (Retired): +"⚠️ WARNING: Asset P1000999 is currently '{{status}}'. Consider retiring it (status=7) instead of deleting. Proceed with deletion? (yes/no)" + +**Asset Creation - Required Fields:** + +When creating asset, ALWAYS require: +- asset_tag (unique identifier) +- display_name (descriptive name) + +Ask user if missing: +"To create asset, I need: +- Asset tag (unique ID) +- Display name +- (Optional) Model, serial number, assigned user, location, status" + +**Field Validation:** + +Install Status: +- Only accept: 1, 6, or 7 +- If user says "active" → 1 +- If user says "available" or "in stock" → 6 +- If user says "retired" or "decommissioned" → 7 + +Dates: +- Format: YYYY-MM-DD +- **CRITICAL: ALWAYS use TODAY'S DATE as baseline for calculations UNLESS user specifies a different date** + +**Date Range Queries (CRITICAL):** + +When querying by date ranges (warranty expiration, maintenance dates, purchase dates): + +1. **Default: Use TODAY'S DATE as baseline** (e.g., if today is 2026-01-27, that's your baseline) +2. **Exception: If user provides a specific date, use that date as baseline** (e.g., "from March 1st" → use 2026-03-01) +3. Calculate the target date based on user request +4. Use ServiceNow date query operators: + - `field>=YYYY-MM-DD^field<=YYYY-MM-DD` - Date range + - `field>=YYYY-MM-DD` - After date + - `field<=YYYY-MM-DD` - Before date + +**Examples:** + +"Warranties expiring in next 90 days" (today = 2026-01-27): +- Baseline: TODAY (2026-01-27) +- Start: 2026-01-27 +- End: 2026-04-27 (today + 90 days) +- Query: `warranty_expiration>=2026-01-27^warranty_expiration<=2026-04-27` + +"Warranties expiring in next 90 days from March 1st" (today = 2026-01-27): +- Baseline: USER-PROVIDED DATE (2026-03-01) +- Start: 2026-03-01 +- End: 2026-05-30 (March 1st + 90 days) +- Query: `warranty_expiration>=2026-03-01^warranty_expiration<=2026-05-30` + +"Maintenance overdue by 6 months" (today = 2026-01-27): +- Threshold: 2025-07-27 (today - 6 months) +- Query: `last_maintenance<=2025-07-27` + +"Assets purchased last month" (today = 2026-01-27): +- Start: 2025-12-27 (30 days ago) +- End: 2026-01-27 (today) +- Query: `purchase_date>=2025-12-27^purchase_date<=2026-01-27` + +**Relative Date Calculations:** +- "today" → Use current date (2026-01-27) +- "tomorrow" → Current date + 1 day +- "next 30 days" → Current date to (current date + 30) +- "last 6 months" → (Current date - 180 days) to current date +- "6 months from today" → Current date to (current date + 180) + +**Key Principle: Results first, no process narration. Always query for sys_id before update/delete operations. Confirm before destructive actions.** diff --git a/docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt b/docs/how-to/agents/ServiceNow/agent_instructions/servicenow_kb_management_agent_instructions.txt similarity index 100% rename from docs/how-to/agents/ServiceNow/servicenow_kb_management_agent_instructions.txt rename to docs/how-to/agents/ServiceNow/agent_instructions/servicenow_kb_management_agent_instructions.txt diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json new file mode 100644 index 00000000..43cb9aab --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json @@ -0,0 +1,231 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ServiceNow Asset Creation API", + "description": "ServiceNow REST API for creating new assets", + "version": "1.0.0", + "contact": { + "name": "ServiceNow API Support", + "url": "https://developer.servicenow.com" + } + }, + "servers": [ + { + "url": "https://dev222288.service-now.com/api/now", + "description": "ServiceNow Instance" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant." + } + }, + "schemas": { + "AssetCreateRequest": { + "type": "object", + "required": ["asset_tag", "display_name"], + "properties": { + "asset_tag": { + "type": "string", + "description": "Unique asset tag identifier (required)", + "example": "P1000500" + }, + "display_name": { + "type": "string", + "description": "Display name for the asset (required)", + "example": "Jane's Laptop" + }, + "model": { + "type": "string", + "description": "Model reference (sys_id or display name)", + "example": "Dell Latitude 5420" + }, + "model_category": { + "type": "string", + "description": "Model category (sys_id or name)", + "example": "Computer" + }, + "serial_number": { + "type": "string", + "description": "Serial number of the asset", + "example": "SN987654321" + }, + "assigned_to": { + "type": "string", + "description": "User assigned to the asset (sys_id or name)", + "example": "Jane Smith" + }, + "assignment_group": { + "type": "string", + "description": "Assignment group (sys_id or name)", + "example": "IT Support" + }, + "location": { + "type": "string", + "description": "Physical location (sys_id or name)", + "example": "Building A, Floor 3" + }, + "install_status": { + "type": "string", + "description": "Installation status: 1=In use, 6=In stock, 7=Retired", + "example": "6", + "enum": ["1", "6", "7"] + }, + "substatus": { + "type": "string", + "description": "Substatus of the asset", + "example": "available" + }, + "purchase_date": { + "type": "string", + "format": "date", + "description": "Purchase date (YYYY-MM-DD)", + "example": "2024-01-15" + }, + "warranty_expiration": { + "type": "string", + "format": "date", + "description": "Warranty expiration date (YYYY-MM-DD)", + "example": "2027-01-15" + }, + "cost": { + "type": "string", + "description": "Asset cost in dollars", + "example": "1500.00" + }, + "department": { + "type": "string", + "description": "Department (sys_id or name)", + "example": "IT" + }, + "managed_by": { + "type": "string", + "description": "Manager of the asset (sys_id or name)", + "example": "John Manager" + }, + "owned_by": { + "type": "string", + "description": "Owner of the asset (sys_id or name)", + "example": "IT Department" + }, + "comments": { + "type": "string", + "description": "Additional comments or notes", + "example": "Purchased for new hire" + } + } + }, + "AssetCreateResponse": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "sys_id": { + "type": "string", + "description": "Newly created asset sys_id" + }, + "asset_tag": { + "type": "string", + "description": "Asset tag" + }, + "display_name": { + "type": "string", + "description": "Display name" + }, + "sys_created_on": { + "type": "string", + "format": "date-time", + "description": "Creation timestamp" + } + } + } + } + } + } + }, + "paths": { + "/table/alm_asset": { + "post": { + "summary": "Create new asset", + "description": "Create a new asset in ServiceNow\n\nRequired fields:\n- asset_tag: Unique asset identifier\n- display_name: Display name for the asset\n\nOptional fields:\n- model: Model reference (sys_id or display name)\n- serial_number: Serial number\n- assigned_to: User reference (sys_id or name)\n- location: Location reference (sys_id or name)\n- install_status: Status (1=In use, 6=In stock, 7=Retired)\n- purchase_date: Purchase date (YYYY-MM-DD)\n- warranty_expiration: Warranty date (YYYY-MM-DD)\n- cost: Asset cost\n- department: Department reference\n- managed_by: Manager reference\n- owned_by: Owner reference\n- comments: Additional notes\n\nThe API will return the newly created asset with its sys_id.", + "operationId": "createAsset", + "requestBody": { + "required": true, + "description": "Asset data to create", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetCreateRequest" + }, + "examples": { + "laptop": { + "summary": "Create laptop asset", + "value": { + "asset_tag": "P1000500", + "display_name": "Jane's Laptop", + "model": "Dell Latitude 5420", + "serial_number": "SN987654321", + "assigned_to": "Jane Smith", + "location": "Building A, Floor 3", + "install_status": "6", + "purchase_date": "2024-01-15", + "warranty_expiration": "2027-01-15", + "cost": "1500.00" + } + }, + "monitor": { + "summary": "Create monitor asset", + "value": { + "asset_tag": "M2000100", + "display_name": "Conference Room Monitor", + "model": "Dell U2720Q", + "serial_number": "MON123456", + "location": "Conference Room A", + "install_status": "1", + "cost": "550.00" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Asset created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetCreateResponse" + } + } + } + }, + "400": { + "description": "Bad Request - Missing required fields or invalid data" + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication token" + }, + "409": { + "description": "Conflict - Asset tag already exists" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json new file mode 100644 index 00000000..59bed115 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json @@ -0,0 +1,73 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ServiceNow Asset Deletion API", + "description": "ServiceNow REST API for deleting assets", + "version": "1.0.0", + "contact": { + "name": "ServiceNow API Support", + "url": "https://developer.servicenow.com" + } + }, + "servers": [ + { + "url": "https://dev222288.service-now.com/api/now", + "description": "ServiceNow Instance" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant." + } + } + }, + "paths": { + "/table/alm_asset/{sys_id}": { + "delete": { + "summary": "Delete asset", + "description": "Delete an asset from ServiceNow\n\n⚠️ CRITICAL: This operation requires the sys_id (NOT the asset tag) ⚠️\n\nThis is a DESTRUCTIVE operation that permanently removes the asset record.\n\n- sys_id is a unique identifier like \"32ac0eaec326361067d91a2ed40131a7\"\n- Asset tag is like \"P1000234\"\n\nREQUIRED EXECUTION PATTERN:\n\nWhen user asks to delete asset by tag (e.g., \"Delete asset P1000234\"):\n\nCALL 1 - Query to Get sys_id:\n1. Call queryAssets(sysparm_query=\"asset_tag=P1000234\", sysparm_fields=\"sys_id,asset_tag,display_name,install_status\")\n2. Extract sys_id from result\n3. Verify exactly 1 result returned\n4. OPTIONAL: Display asset details to user for confirmation before deletion\n\nCALL 2 - Delete with Retrieved sys_id:\n1. Call deleteAsset(sys_id=\"32ac0eaec326361067d91a2ed40131a7\")\n2. This will permanently delete the asset\n\nBest Practices:\n- Always confirm with user before deleting\n- Show asset details (tag, name, assigned user) before deletion\n- Consider retiring assets (install_status=7) instead of deleting\n- Deletion is permanent and cannot be undone\n- Check if asset has related records (CIs, contracts) that may be affected\n\nPermissions:\n- User must have delete rights on alm_asset table\n- Some organizations restrict asset deletion for compliance/audit reasons", + "operationId": "deleteAsset", + "parameters": [ + { + "name": "sys_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique sys_id of the asset to delete (obtain via queryAssets first)", + "example": "32ac0eaec326361067d91a2ed40131a7" + } + ], + "responses": { + "204": { + "description": "Asset deleted successfully - No content returned" + }, + "404": { + "description": "Asset not found - Invalid sys_id or already deleted" + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication token" + }, + "403": { + "description": "Forbidden - User lacks delete permissions on alm_asset table" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json new file mode 100644 index 00000000..948e1612 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json @@ -0,0 +1,283 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ServiceNow Asset Query API", + "description": "ServiceNow REST API for querying assets and retrieving asset details", + "version": "1.0.0", + "contact": { + "name": "ServiceNow API Support", + "url": "https://developer.servicenow.com" + } + }, + "servers": [ + { + "url": "https://dev222288.service-now.com/api/now", + "description": "ServiceNow Instance" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant." + } + }, + "schemas": { + "Asset": { + "type": "object", + "properties": { + "sys_id": { + "type": "string", + "description": "Unique system identifier", + "example": "abc123xyz789" + }, + "asset_tag": { + "type": "string", + "description": "Asset tag/identifier", + "example": "P1000234" + }, + "display_name": { + "type": "string", + "description": "Display name of the asset", + "example": "John Doe's Laptop" + }, + "model": { + "type": "string", + "description": "Asset model", + "example": "Dell Latitude 5420" + }, + "model_category": { + "type": "string", + "description": "Model category", + "example": "Computer" + }, + "serial_number": { + "type": "string", + "description": "Serial number", + "example": "SN123456789" + }, + "assigned_to": { + "type": "string", + "description": "User assigned to the asset", + "example": "John Doe" + }, + "assignment_group": { + "type": "string", + "description": "Assignment group", + "example": "IT Support" + }, + "location": { + "type": "string", + "description": "Physical location", + "example": "Building A, Floor 3" + }, + "install_status": { + "type": "string", + "description": "Installation status: 1=In use, 6=In stock, 7=Retired", + "example": "1" + }, + "substatus": { + "type": "string", + "description": "Substatus of the asset", + "example": "available" + }, + "purchase_date": { + "type": "string", + "format": "date", + "description": "Purchase date", + "example": "2023-01-15" + }, + "warranty_expiration": { + "type": "string", + "format": "date", + "description": "Warranty expiration date", + "example": "2026-01-15" + }, + "cost": { + "type": "string", + "description": "Asset cost", + "example": "1200.00" + }, + "sys_created_on": { + "type": "string", + "format": "date-time", + "description": "Creation timestamp", + "example": "2023-01-15T10:30:00" + }, + "sys_updated_on": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp", + "example": "2023-06-20T14:22:00" + } + } + } + } + }, + "paths": { + "/table/alm_asset": { + "get": { + "summary": "Query assets", + "description": "Retrieve assets from ServiceNow based on query parameters and filters\n\nParameters:\n- sysparm_query (optional): Encoded query string for filtering results. Examples:\n - By asset tag: asset_tag=P1000234\n - By assigned user: assigned_to.name=John Doe\n - By model: model.display_name=Dell Latitude\n - By install status: install_status=1 (1=In use, 6=In stock, 7=Retired)\n - Combined: install_status=1^assigned_to.name=John Doe\n\n- sysparm_limit (optional): Maximum number of records to return\n- sysparm_offset (optional): Number of records to skip for pagination\n- sysparm_fields (optional): Comma-separated list of fields to return.\n CRITICAL: Always include sys_id field - it's required for follow-up queries.\n\n- sysparm_display_value (optional): Return display values instead of actual values", + "operationId": "queryAssets", + "parameters": [ + { + "name": "sysparm_query", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Encoded query string for filtering", + "example": "install_status=1" + }, + { + "name": "sysparm_limit", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Maximum number of records to return", + "example": 50 + }, + { + "name": "sysparm_offset", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Number of records to skip for pagination", + "example": 0 + }, + { + "name": "sysparm_fields", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Comma-separated list of fields to return", + "example": "sys_id,asset_tag,display_name,assigned_to,install_status" + }, + { + "name": "sysparm_display_value", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + }, + "description": "Return display values instead of sys_ids", + "example": true + } + ], + "responses": { + "200": { + "description": "Assets retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Asset" + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication token" + }, + "400": { + "description": "Bad Request - Invalid query parameters" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/table/alm_asset/{sys_id}": { + "get": { + "summary": "Get asset details", + "description": "Retrieve details of a specific asset.\n\nCRITICAL: This operation requires the sys_id (NOT the asset tag).\n- sys_id is a unique identifier like \"9d385017c611228701d22104cc95c371\"\n- Asset tag is like \"P1000234\"\n\nWhen user asks about an asset by tag:\n1. First use queryAssets with sysparm_query=asset_tag=P1000234\n2. Get the sys_id from the result\n3. Then call getAssetDetails with that sys_id\n\nParameters:\n- sys_id (required): The sys_id of the asset\n- sysparm_fields (optional): Comma-separated list of fields to return\n- sysparm_display_value (optional): Return display values", + "operationId": "getAssetDetails", + "parameters": [ + { + "name": "sys_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique sys_id of the asset", + "example": "9d385017c611228701d22104cc95c371" + }, + { + "name": "sysparm_fields", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Comma-separated list of fields to return", + "example": "sys_id,asset_tag,display_name,model,serial_number,assigned_to,location,install_status" + }, + { + "name": "sysparm_display_value", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + }, + "description": "Return display values instead of sys_ids", + "example": true + } + ], + "responses": { + "200": { + "description": "Asset details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/Asset" + } + } + } + } + } + }, + "404": { + "description": "Asset not found" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +} diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json new file mode 100644 index 00000000..c8aff343 --- /dev/null +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json @@ -0,0 +1,224 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ServiceNow Asset Update API", + "description": "ServiceNow REST API for updating existing assets", + "version": "1.0.0", + "contact": { + "name": "ServiceNow API Support", + "url": "https://developer.servicenow.com" + } + }, + "servers": [ + { + "url": "https://dev222288.service-now.com/api/now", + "description": "ServiceNow Instance" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "OAuth 2.0 Bearer Token authentication. Obtain token via ServiceNow OAuth endpoint using Resource Owner Password Credentials grant." + } + }, + "schemas": { + "AssetUpdateRequest": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "description": "Update display name", + "example": "Bob's Laptop (Updated)" + }, + "assigned_to": { + "type": "string", + "description": "Update assigned user (sys_id or name)", + "example": "Bob Johnson" + }, + "assignment_group": { + "type": "string", + "description": "Update assignment group", + "example": "IT Support" + }, + "location": { + "type": "string", + "description": "Update location (sys_id or name)", + "example": "Building B, Floor 2" + }, + "install_status": { + "type": "string", + "description": "Update status: 1=In use, 6=In stock, 7=Retired", + "example": "1", + "enum": ["1", "6", "7"] + }, + "substatus": { + "type": "string", + "description": "Update substatus", + "example": "in_use" + }, + "serial_number": { + "type": "string", + "description": "Update serial number", + "example": "SN999888777" + }, + "warranty_expiration": { + "type": "string", + "format": "date", + "description": "Update warranty expiration (YYYY-MM-DD)", + "example": "2028-01-15" + }, + "cost": { + "type": "string", + "description": "Update cost", + "example": "1800.00" + }, + "department": { + "type": "string", + "description": "Update department", + "example": "Engineering" + }, + "managed_by": { + "type": "string", + "description": "Update manager", + "example": "New Manager" + }, + "owned_by": { + "type": "string", + "description": "Update owner", + "example": "Engineering Department" + }, + "comments": { + "type": "string", + "description": "Update or add comments", + "example": "Reassigned to Engineering team" + } + } + }, + "AssetUpdateResponse": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "sys_id": { + "type": "string", + "description": "Asset sys_id" + }, + "asset_tag": { + "type": "string", + "description": "Asset tag" + }, + "display_name": { + "type": "string", + "description": "Updated display name" + }, + "sys_updated_on": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + } + } + } + } + } + }, + "paths": { + "/table/alm_asset/{sys_id}": { + "patch": { + "summary": "Update asset", + "description": "Update asset fields\n\n⚠️ CRITICAL: YOU MUST QUERY FOR sys_id FIRST - DO NOT USE CACHED VALUES ⚠️\n\nThis operation requires the sys_id (NOT the asset tag).\n- sys_id is a unique identifier like \"32ac0eaec326361067d91a2ed40131a7\"\n- Asset tag is like \"P1000234\"\n\nREQUIRED EXECUTION PATTERN:\n\nWhen user asks to update asset P1000234:\n\nCALL 1 - Query to Get sys_id:\n1. Call queryAssets(sysparm_query=\"asset_tag=P1000234\", sysparm_fields=\"sys_id,asset_tag\")\n2. Extract sys_id from result\n3. Verify you got exactly 1 result\n\nCALL 2 - Update with Retrieved sys_id:\n1. Call updateAsset(sys_id=\"32ac0eaec326361067d91a2ed40131a7\", ...fields)\n2. This will succeed because you have the correct sys_id\n\nUpdatable Fields:\n- display_name: Asset display name\n- assigned_to: User assignment\n- assignment_group: Group assignment\n- location: Physical location\n- install_status: Status (1=In use, 6=In stock, 7=Retired)\n- substatus: Substatus\n- serial_number: Serial number\n- warranty_expiration: Warranty date\n- cost: Asset cost\n- department: Department\n- managed_by: Manager\n- owned_by: Owner\n- comments: Notes", + "operationId": "updateAsset", + "parameters": [ + { + "name": "sys_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique sys_id of the asset to update (obtain via queryAssets first)", + "example": "32ac0eaec326361067d91a2ed40131a7" + } + ], + "requestBody": { + "required": true, + "description": "Fields to update (only include fields you want to change)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetUpdateRequest" + }, + "examples": { + "reassign": { + "summary": "Reassign asset to new user", + "value": { + "assigned_to": "Bob Johnson", + "install_status": "1", + "comments": "Reassigned to Bob" + } + }, + "relocate": { + "summary": "Move asset to new location", + "value": { + "location": "Building B, Floor 5", + "comments": "Moved to new office" + } + }, + "retire": { + "summary": "Retire asset", + "value": { + "install_status": "7", + "assigned_to": "", + "comments": "Asset retired - end of life" + } + }, + "update_warranty": { + "summary": "Extend warranty", + "value": { + "warranty_expiration": "2028-12-31", + "comments": "Warranty extended" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Asset updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetUpdateResponse" + } + } + } + }, + "404": { + "description": "Asset not found - Invalid sys_id" + }, + "400": { + "description": "Bad Request - Invalid field values" + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication token" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +} From b5804ad5f78dfe1c6c857e7ed94a2a79a1cf6312 Mon Sep 17 00:00:00 2001 From: "Chen, Vivien" Date: Wed, 28 Jan 2026 11:04:01 -0500 Subject: [PATCH 35/35] Remove any references to actual ServiceNow instances --- .../ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md | 8 ++++---- .../open_api_specs/servicenow_create_asset_openapi.json | 2 +- .../open_api_specs/servicenow_delete_asset_openapi.json | 2 +- .../open_api_specs/servicenow_query_assets_openapi.json | 2 +- .../open_api_specs/servicenow_update_asset_openapi.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md b/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md index c56767f7..e99f67f7 100644 --- a/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md +++ b/docs/how-to/agents/ServiceNow/SERVICENOW_ASSET_MANAGEMENT_SETUP.md @@ -124,7 +124,7 @@ When creating each action (query, create, update, delete), use the same authenti - `queryAssets` - Search and filter assets with query parameters - `getAssetDetails` - Retrieve full details for a specific asset by sys_id -**Endpoint:** `https://dev222288.service-now.com/api/now` +**Endpoint:** `https://YOUR-INSTANCE.service-now.com/api/now` **Authentication:** - Type: `key` @@ -150,7 +150,7 @@ When creating each action (query, create, update, delete), use the same authenti **Optional Fields:** model, serial_number, assigned_to, location, install_status, purchase_date, warranty_expiration, cost, department, managed_by, owned_by, comments -**Endpoint:** `https://dev222288.service-now.com/api/now` +**Endpoint:** `https://YOUR-INSTANCE.service-now.com/api/now` --- @@ -170,7 +170,7 @@ When creating each action (query, create, update, delete), use the same authenti **Updatable Fields:** display_name, assigned_to, assignment_group, location, install_status, substatus, serial_number, warranty_expiration, cost, department, managed_by, owned_by, comments -**Endpoint:** `https://dev222288.service-now.com/api/now` +**Endpoint:** `https://YOUR-INSTANCE.service-now.com/api/now` --- @@ -190,7 +190,7 @@ When creating each action (query, create, update, delete), use the same authenti **⚠️ CRITICAL:** Query for sys_id first using asset_tag, then delete -**Endpoint:** `https://dev222288.service-now.com/api/now` +**Endpoint:** `https://YOUR-INSTANCE.service-now.com/api/now` --- diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json index 43cb9aab..9fe33fa7 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_create_asset_openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://dev222288.service-now.com/api/now", + "url": "https://YOUR-INSTANCE.service-now.com/api/now", "description": "ServiceNow Instance" } ], diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json index 59bed115..ef861467 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_delete_asset_openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://dev222288.service-now.com/api/now", + "url": "https://YOUR-INSTANCE.service-now.com/api/now", "description": "ServiceNow Instance" } ], diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json index 948e1612..f8ed10c9 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_query_assets_openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://dev222288.service-now.com/api/now", + "url": "https://YOUR-INSTANCE.service-now.com/api/now", "description": "ServiceNow Instance" } ], diff --git a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json index c8aff343..c42bdf1f 100644 --- a/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json +++ b/docs/how-to/agents/ServiceNow/open_api_specs/servicenow_update_asset_openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://dev222288.service-now.com/api/now", + "url": "https://YOUR-INSTANCE.service-now.com/api/now", "description": "ServiceNow Instance" } ],