From 6dbb107733704326c975bd7ffa4e792d5d4a90ad Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 22 Jan 2026 16:00:37 -0500 Subject: [PATCH 1/9] Updated release notes --- .../v0.236.011/AGENT_TEMPLATE_GALLERY.md | 197 +++++++++++++++ .../AZURE_AI_FOUNDRY_AGENT_SUPPORT.md | 235 ++++++++++++++++++ .../v0.236.011/CONVERSATION_DEEP_LINKING.md | 141 +++++++++++ .../PLUGIN_AUTH_TYPE_CONSTRAINTS.md | 202 +++++++++++++++ .../v0.236.011/WEB_SEARCH_AZURE_AI_FOUNDRY.md | 119 +++++++++ .../AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md | 0 .../AGENT_TEMPLATE_MAX_LENGTHS_FIX.md | 0 .../CONTROL_CENTER_DATE_LABELS_FIX.md | 0 docs/explanation/release_notes.md | 57 +++++ 9 files changed, 951 insertions(+) create mode 100644 docs/explanation/features/v0.236.011/AGENT_TEMPLATE_GALLERY.md create mode 100644 docs/explanation/features/v0.236.011/AZURE_AI_FOUNDRY_AGENT_SUPPORT.md create mode 100644 docs/explanation/features/v0.236.011/CONVERSATION_DEEP_LINKING.md create mode 100644 docs/explanation/features/v0.236.011/PLUGIN_AUTH_TYPE_CONSTRAINTS.md create mode 100644 docs/explanation/features/v0.236.011/WEB_SEARCH_AZURE_AI_FOUNDRY.md rename docs/explanation/fixes/{ => v0.236.011}/AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md (100%) rename docs/explanation/fixes/{ => v0.236.011}/AGENT_TEMPLATE_MAX_LENGTHS_FIX.md (100%) rename docs/explanation/fixes/{ => v0.236.011}/CONTROL_CENTER_DATE_LABELS_FIX.md (100%) diff --git a/docs/explanation/features/v0.236.011/AGENT_TEMPLATE_GALLERY.md b/docs/explanation/features/v0.236.011/AGENT_TEMPLATE_GALLERY.md new file mode 100644 index 00000000..ecd79217 --- /dev/null +++ b/docs/explanation/features/v0.236.011/AGENT_TEMPLATE_GALLERY.md @@ -0,0 +1,197 @@ +# Agent Template Gallery + +## Overview + +The Agent Template Gallery provides a curated collection of reusable agent configurations that users can use as starting points when creating new agents. This feature includes an admin review workflow to ensure quality control and organizational compliance for user-submitted templates. + +**Version Implemented:** 0.236.011 + +## Key Features + +- **Template Gallery**: Browse and select from approved agent templates +- **User Submission**: Users can submit their agents as templates for community use +- **Admin Review Workflow**: Submitted templates require admin approval before publication +- **Rich Metadata**: Templates include titles, descriptions, tags, and instructions +- **Helper Text**: Short descriptions optimized for gallery display +- **Actions Integration**: Templates can include predefined actions/plugins + +## Configuration + +### Admin Settings + +| Setting | Description | Default | +|---------|-------------|---------| +| `enable_agent_template_gallery` | Show template gallery in agent builder | `true` | +| `agent_templates_allow_user_submission` | Allow users to submit templates | `true` | +| `agent_templates_require_approval` | Require admin approval for submissions | `true` | + +### Accessing Template Gallery Settings + +1. Navigate to **Admin Settings** from the sidebar +2. Select the **Agents** tab +3. Locate the **Agent Template Gallery** section + +## Template Workflow + +### Template Statuses + +| Status | Description | +|--------|-------------| +| `pending` | Submitted, awaiting admin review | +| `approved` | Approved and visible in gallery | +| `rejected` | Rejected by admin | +| `archived` | Removed from gallery but preserved | + +### User Submission Flow + +1. User creates and configures an agent +2. User selects "Submit as Template" option +3. User provides additional metadata (title, description, tags) +4. Template is saved with `pending` status +5. Template awaits admin review + +### Admin Review Flow + +1. Admin navigates to template management +2. Admin reviews pending templates +3. Admin can: + - **Approve**: Template becomes visible in gallery + - **Reject**: Template is marked rejected with reason + - **Edit**: Modify template details before approval + +## Template Structure + +### Required Fields + +| Field | Max Length | Description | +|-------|------------|-------------| +| `display_name` / `title` | 200 chars | Template name | +| `description` | 2000 chars | Full description | +| `instructions` | 30000 chars | Agent instructions | + +### Optional Fields + +| Field | Max Length | Description | +|-------|------------|-------------| +| `helper_text` | 140 chars | Short gallery description | +| `tags` | 64 chars each | Categorization tags | +| `actions_to_load` | 128 chars each | Predefined actions | +| `additional_settings` | JSON | Extra configuration | + +### Helper Text Generation + +If no explicit helper text is provided, it's automatically generated from the description: +- If description ≤ 140 characters: Use full description +- If description > 140 characters: Truncate to 137 characters + "..." + +## Technical Architecture + +### Backend Components + +| File | Purpose | +|------|---------| +| [functions_agent_templates.py](../../../../application/single_app/functions_agent_templates.py) | CRUD operations for templates | +| [config.py](../../../../application/single_app/config.py) | Cosmos DB container setup | +| [route_frontend_admin_settings.py](../../../../application/single_app/route_frontend_admin_settings.py) | Settings management | + +### Cosmos DB Container + +```python +cosmos_agent_templates_container_name = "agent_templates" +cosmos_agent_templates_container = cosmos_database.create_container_if_not_exists( + id=cosmos_agent_templates_container_name, + partition_key=PartitionKey(path='/id') +) +``` + +### Key Functions + +| Function | Purpose | +|----------|---------| +| `list_agent_templates(status, include_internal)` | List templates with optional filtering | +| `get_agent_template(template_id)` | Get single template by ID | +| `create_agent_template(payload, user_info, auto_approve)` | Create new template | +| `update_agent_template(template_id, updates)` | Update existing template | +| `validate_template_payload(payload)` | Validate template data | + +### API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/agent-templates` | GET | List approved templates | +| `/api/agent-templates` | POST | Submit new template | +| `/api/agent-templates/` | GET | Get template details | +| `/api/agent-templates/` | PUT | Update template | + +## User Experience + +### Browsing Templates + +1. Open Agent Builder modal +2. View Template Gallery section +3. Browse by title, description, or tags +4. Click template to view details +5. Select "Use Template" to create agent from template + +### Submitting Templates + +1. Create and test an agent +2. Click "Submit as Template" +3. Fill in template metadata: + - Title/display name + - Description + - Helper text (optional) + - Tags + - Submission notes +4. Submit for review + +### Template Card Display + +Each template card shows: +- Title +- Helper text (short description) +- Tags +- Action count +- "Use Template" button + +## Activity Logging + +Template submissions and approvals are logged: +```python +log_event( + "Agent template submitted", + extra={ + "template_id": template['id'], + "status": template['status'], + "created_by": template.get('created_by'), + }, +) +``` + +## Security Considerations + +1. **Admin Approval**: Templates must be reviewed before publication +2. **User Attribution**: Submissions track creator identity +3. **Content Sanitization**: Template fields are sanitized and length-limited +4. **Permission Control**: Only admins can approve/reject templates +5. **Internal Fields**: Sensitive fields are stripped from public views + +## Validation Rules + +Templates are validated for: +- Required fields presence (display_name, description, instructions) +- Maximum field lengths +- Valid JSON for additional_settings +- Proper tag formatting + +## Known Limitations + +- Templates don't include file attachments +- Actions referenced must exist in the system +- Large instruction sets may affect load times + +## Related Features + +- Agent Builder +- Personal and Group Agents +- Plugin/Action Configuration diff --git a/docs/explanation/features/v0.236.011/AZURE_AI_FOUNDRY_AGENT_SUPPORT.md b/docs/explanation/features/v0.236.011/AZURE_AI_FOUNDRY_AGENT_SUPPORT.md new file mode 100644 index 00000000..ac4d0893 --- /dev/null +++ b/docs/explanation/features/v0.236.011/AZURE_AI_FOUNDRY_AGENT_SUPPORT.md @@ -0,0 +1,235 @@ +# Azure AI Foundry Agent Support + +## Overview + +SimpleChat now supports Azure AI Foundry agents as a first-class agent type. This integration allows users to leverage pre-built agents from Azure AI Foundry (formerly Azure AI Agent Service) directly within SimpleChat, combining the power of cloud-hosted AI agents with SimpleChat's conversation management and workspace features. + +**Version Implemented:** 0.236.011 + +## Key Features + +- **Native Agent Type**: `aifoundry` agent type in agent configuration +- **Semantic Kernel Integration**: Uses Semantic Kernel's `AzureAIAgent` for execution +- **Credential Management**: Supports Azure Default Credential and Client Secret authentication +- **Citation Support**: Captures and displays citations from Foundry agent responses +- **Model Tracking**: Tracks which model was used for each response + +## Configuration + +### Agent Configuration Structure + +Azure AI Foundry agents are configured with the `agent_type: "aifoundry"` setting: + +```json +{ + "name": "my-foundry-agent", + "display_name": "My Foundry Agent", + "description": "An agent powered by Azure AI Foundry", + "agent_type": "aifoundry", + "other_settings": { + "azure_ai_foundry": { + "agent_id": "asst_xxxxxxxxxxxxxx", + "api_version": "2024-12-01-preview" + } + } +} +``` + +### Required Settings + +| Setting | Location | Description | +|---------|----------|-------------| +| `agent_id` | `other_settings.azure_ai_foundry.agent_id` | The Azure AI Foundry agent ID | +| Endpoint | Global settings or Foundry settings | Azure AI Foundry endpoint URL | + +### Optional Settings + +| Setting | Description | +|---------|-------------| +| `api_version` | API version to use (default from global settings) | +| Credential settings | For client secret authentication | + +## Technical Architecture + +### Core Components + +| File | Purpose | +|------|---------| +| [foundry_agent_runtime.py](../../../../application/single_app/foundry_agent_runtime.py) | Agent execution and Semantic Kernel integration | + +### Key Classes + +#### `AzureAIFoundryChatCompletionAgent` + +Lightweight wrapper that makes Foundry agents behave like Semantic Kernel chat agents: + +```python +class AzureAIFoundryChatCompletionAgent: + """Lightweight wrapper so Foundry agents behave like SK chat agents.""" + + agent_type = "aifoundry" + + def __init__(self, agent_config: Dict[str, Any], settings: Dict[str, Any]): + self.name = agent_config.get("name") + self.display_name = agent_config.get("display_name") or self.name + self.description = agent_config.get("description", "") + # ... additional properties + + def invoke(self, agent_message_history, metadata=None) -> str: + """Synchronously invoke the Foundry agent.""" + # Executes async Foundry call and returns response text +``` + +#### `FoundryAgentInvocationResult` + +Data class representing the outcome from a Foundry agent run: + +```python +@dataclass +class FoundryAgentInvocationResult: + message: str # The response text + model: Optional[str] # Model used for response + citations: List[Dict] # Any citations from the response + metadata: Dict # Additional metadata +``` + +### Execution Flow + +1. **Agent Invocation**: `invoke()` method is called with message history +2. **Async Execution**: `execute_foundry_agent()` handles the actual API call +3. **Client Creation**: `AzureAIAgent.create_client()` sets up authenticated client +4. **Agent Retrieval**: Fetches agent definition from Foundry +5. **Message Processing**: Sends messages and collects responses +6. **Result Extraction**: Captures message, model, and citations + +### Authentication + +The runtime supports multiple authentication methods: + +1. **Default Azure Credential**: Uses `DefaultAzureCredential` for managed identity +2. **Client Secret**: Uses `ClientSecretCredential` for service principal auth +3. **Key Vault Integration**: Secrets can be retrieved from Azure Key Vault + +```python +credential = _build_async_credential(foundry_settings, global_settings) +client = AzureAIAgent.create_client( + credential=credential, + endpoint=endpoint, + api_version=api_version, +) +``` + +## Admin Configuration + +### Creating a Foundry Agent + +1. Navigate to **Agent Builder** or **Admin Settings → Agents** +2. Select **Create New Agent** +3. Choose **Azure AI Foundry** as the agent type +4. Enter the **Agent ID** from Azure AI Foundry +5. Configure any additional settings +6. Save the agent + +### Global Settings + +Azure AI Foundry settings can be configured at the global level: + +| Setting | Description | +|---------|-------------| +| `azure_ai_foundry_endpoint` | Default Foundry endpoint | +| `azure_ai_foundry_api_version` | Default API version | + +## User Experience + +### For End Users + +- Foundry agents appear alongside other agents in the agent selector +- Chat interactions work identically to other agent types +- Responses may include citations from Foundry agent capabilities + +### For Administrators + +- Full control over which Foundry agents are available +- Can configure as global, group, or personal agents +- Monitor usage through activity logging + +## Error Handling + +### `FoundryAgentInvocationError` + +Raised when the Foundry agent invocation cannot be completed: + +```python +class FoundryAgentInvocationError(RuntimeError): + """Raised when the Foundry agent invocation cannot be completed.""" +``` + +Common causes: +- Missing agent_id configuration +- Invalid credentials +- Network connectivity issues +- Foundry service unavailable + +### Validation + +The runtime validates required configuration: + +```python +agent_id = (foundry_settings.get("agent_id") or "").strip() +if not agent_id: + raise FoundryAgentInvocationError( + "Azure AI Foundry agents require an agent_id in other_settings.azure_ai_foundry." + ) +``` + +## Logging and Monitoring + +### Event Logging + +Agent invocations are logged to Application Insights: + +```python +log_event( + "[FoundryAgent] Invocation runtime error", + extra={ + "agent_id": self.id, + "agent_name": self.name, + }, + level=logging.ERROR, +) +``` + +### Debug Output + +Debug information is available during development: + +```python +debug_print( + f"[FoundryAgent] Invoking agent '{self.name}' with {len(history)} messages" +) +``` + +## Security Considerations + +1. **Credential Security**: Use managed identity when possible +2. **Key Vault**: Store secrets in Azure Key Vault +3. **Access Control**: Control which users can access Foundry agents +4. **Data Boundaries**: Be aware of data processing in Azure AI Foundry + +## Dependencies + +- `semantic-kernel`: For `AzureAIAgent` abstraction +- `azure-identity`: For Azure authentication +- Azure AI Foundry account with configured agents + +## Known Limitations + +- Plugins attached to SimpleChat agents are not passed to Foundry agents +- APIM metadata is stripped for Foundry agent calls +- Streaming responses are collected before returning + +## Related Features + +- [Web Search via Azure AI Foundry](WEB_SEARCH_AZURE_AI_FOUNDRY.md) +- Agent Builder +- Plugin Management diff --git a/docs/explanation/features/v0.236.011/CONVERSATION_DEEP_LINKING.md b/docs/explanation/features/v0.236.011/CONVERSATION_DEEP_LINKING.md new file mode 100644 index 00000000..d3c6e53e --- /dev/null +++ b/docs/explanation/features/v0.236.011/CONVERSATION_DEEP_LINKING.md @@ -0,0 +1,141 @@ +# Conversation Deep Linking + +## Overview + +SimpleChat now supports conversation deep linking through URL query parameters. Users can share direct links to specific conversations, and the application will automatically navigate to and load the referenced conversation when the link is accessed. + +**Version Implemented:** 0.236.011 + +## Key Features + +- **Direct Conversation Links**: Share URLs that open a specific conversation +- **URL Parameter Support**: Supports both `conversationId` and `conversation_id` parameters +- **Automatic URL Updates**: Current conversation ID is automatically added to the URL +- **Browser History Integration**: Uses `replaceState` to update URLs without creating new history entries +- **Error Handling**: Graceful handling of invalid or inaccessible conversation IDs + +## How It Works + +### URL Format + +Conversations can be linked using either parameter format: + +``` +https://your-simplechat.com/?conversationId= +https://your-simplechat.com/?conversation_id= +``` + +### Automatic URL Updates + +When users select a conversation in the sidebar, the URL is automatically updated to include the conversation ID: + +```javascript +function updateConversationUrl(conversationId) { + if (!conversationId) return; + + try { + const url = new URL(window.location.href); + url.searchParams.set('conversationId', conversationId); + window.history.replaceState({}, '', url.toString()); + } catch (error) { + console.warn('Failed to update conversation URL:', error); + } +} +``` + +### Deep Link Loading + +On page load, the application checks for a `conversationId` parameter and loads that conversation: + +```javascript +// Deep-link: conversationId query param +const conversationId = getUrlParameter("conversationId") || getUrlParameter("conversation_id"); +if (conversationId) { + try { + await ensureConversationPresent(conversationId); + await selectConversation(conversationId); + } catch (err) { + console.error('Failed to load conversation from URL param:', err); + showToast('Could not open that conversation.', 'danger'); + } +} +``` + +## User Experience + +### Sharing Conversations + +1. Navigate to any conversation +2. Copy the URL from the browser address bar +3. Share the URL with colleagues +4. Recipients with access can open the link to view the conversation + +### Receiving Shared Links + +1. Click or paste a shared conversation link +2. The application loads and displays the referenced conversation +3. If the conversation doesn't exist or isn't accessible, an error toast is shown + +### Error Handling + +When a deep link fails to load: +- A toast notification appears: "Could not open that conversation." +- The user remains on the default view +- Console logging captures the error details for debugging + +## Technical Architecture + +### Frontend Components + +| File | Purpose | +|------|---------| +| [chat-onload.js](../../../../application/single_app/static/js/chat/chat-onload.js) | Handles deep link loading on page initialization | +| [chat-conversations.js](../../../../application/single_app/static/js/chat/chat-conversations.js) | `updateConversationUrl()` function for URL management | + +### Functions Involved + +| Function | Purpose | +|----------|---------| +| `getUrlParameter(name)` | Retrieves query parameter value from current URL | +| `ensureConversationPresent(id)` | Ensures conversation exists in the local list | +| `selectConversation(id)` | Loads and displays the specified conversation | +| `updateConversationUrl(id)` | Updates URL with current conversation ID | + +## Use Cases + +### Team Collaboration +- Share conversation links in chat or email for review +- Direct colleagues to specific AI interactions for discussion + +### Support and Troubleshooting +- Users can share conversation links with support staff +- Administrators can reference specific conversations in reports + +### Documentation +- Bookmark important conversations for future reference +- Create documentation links to example interactions + +## Security Considerations + +1. **Access Control**: Deep links respect existing conversation access permissions +2. **User Ownership**: Only accessible if the user has rights to the conversation +3. **No Authentication Bypass**: Users must still be logged in to access conversations +4. **Workspace Boundaries**: Workspace permissions still apply + +## Browser Compatibility + +- Uses standard `URL` and `URLSearchParams` APIs +- `history.replaceState()` for seamless URL updates +- Compatible with all modern browsers + +## Known Limitations + +- Deep links only work for conversations the current user has access to +- Links to deleted conversations will show an error +- Group/public workspace conversations require appropriate membership + +## Related Features + +- Conversation management and history +- Sidebar conversation navigation +- Chat workspace functionality diff --git a/docs/explanation/features/v0.236.011/PLUGIN_AUTH_TYPE_CONSTRAINTS.md b/docs/explanation/features/v0.236.011/PLUGIN_AUTH_TYPE_CONSTRAINTS.md new file mode 100644 index 00000000..093923c6 --- /dev/null +++ b/docs/explanation/features/v0.236.011/PLUGIN_AUTH_TYPE_CONSTRAINTS.md @@ -0,0 +1,202 @@ +# Plugin Authentication Type Constraints + +## Overview + +SimpleChat now enforces authentication type constraints per plugin type. Different plugin types may support different authentication methods based on their requirements and the APIs they integrate with. This feature provides a structured way to define and retrieve allowed authentication types for each plugin type. + +**Version Implemented:** 0.236.011 + +## Key Features + +- **Per-Plugin Auth Types**: Each plugin type can define its own allowed authentication types +- **Schema-Based Defaults**: Falls back to global AuthType enum from plugin.schema.json +- **Definition File Overrides**: Plugin-specific definition files can restrict available auth types +- **API Endpoint**: RESTful endpoint to query allowed auth types for any plugin type + +## How It Works + +### Authentication Type Resolution + +The system resolves allowed authentication types in this order: + +1. **Check Plugin Definition File**: `{plugin_type}.definition.json` + - If `allowedAuthTypes` array exists and is non-empty, use it +2. **Fallback to Global Schema**: `plugin.schema.json` + - Use the `AuthType` enum from definitions + +### API Endpoint + +``` +GET /api/plugins/{plugin_type}/auth-types +``` + +**Response:** +```json +{ + "allowedAuthTypes": ["none", "api_key", "oauth2", "basic"], + "source": "definition" +} +``` + +**Response Fields:** +| Field | Description | +|-------|-------------| +| `allowedAuthTypes` | Array of allowed authentication type strings | +| `source` | Where the types came from: "definition" or "schema" | + +## Configuration Files + +### Plugin Schema (Global Defaults) + +Location: `static/json/schemas/plugin.schema.json` + +```json +{ + "definitions": { + "AuthType": { + "enum": ["none", "api_key", "oauth2", "basic", "bearer", "custom"] + } + } +} +``` + +### Plugin Definition Files (Per-Plugin Overrides) + +Location: `static/json/schemas/{plugin_type}.definition.json` + +Example for a plugin that only supports API key authentication: + +```json +{ + "name": "weather_plugin", + "displayName": "Weather API", + "description": "Get weather information", + "allowedAuthTypes": ["none", "api_key"] +} +``` + +## Technical Architecture + +### Backend Implementation + +Location: [route_backend_plugins.py](../../../../application/single_app/route_backend_plugins.py) + +```python +@bpap.route('/api/plugins//auth-types', methods=['GET']) +@login_required +@user_required +def get_plugin_auth_types(plugin_type): + """ + Returns allowed auth types for a plugin type. Uses definition file if present, + otherwise falls back to AuthType enum in plugin.schema.json. + """ + schema_dir = os.path.join(current_app.root_path, 'static', 'json', 'schemas') + safe_type = re.sub(r'[^a-zA-Z0-9_]', '_', plugin_type).lower() + + # Try to load from plugin definition file + definition_path = os.path.join(schema_dir, f'{safe_type}.definition.json') + schema_path = os.path.join(schema_dir, 'plugin.schema.json') + + allowed_auth_types = [] + source = "schema" + + # Load defaults from schema + try: + with open(schema_path, 'r', encoding='utf-8') as schema_file: + schema = json.load(schema_file) + allowed_auth_types = ( + schema + .get('definitions', {}) + .get('AuthType', {}) + .get('enum', []) + ) + except Exception: + allowed_auth_types = [] + + # Override with definition file if present + if os.path.exists(definition_path): + try: + with open(definition_path, 'r', encoding='utf-8') as definition_file: + definition = json.load(definition_file) + allowed_from_definition = definition.get('allowedAuthTypes') + if isinstance(allowed_from_definition, list) and allowed_from_definition: + allowed_auth_types = allowed_from_definition + source = "definition" + except Exception: + pass + + return jsonify({ + "allowedAuthTypes": allowed_auth_types, + "source": source + }) +``` + +### Security + +- Plugin type is sanitized to prevent path traversal +- Only alphanumeric characters and underscores are allowed in plugin type names +- Endpoint requires user authentication + +## Common Authentication Types + +| Type | Description | Use Case | +|------|-------------|----------| +| `none` | No authentication required | Public APIs | +| `api_key` | API key in header or query | Most REST APIs | +| `oauth2` | OAuth 2.0 flow | Microsoft Graph, Google APIs | +| `basic` | Basic HTTP authentication | Legacy systems | +| `bearer` | Bearer token authentication | JWT-based APIs | +| `custom` | Custom authentication handler | Special requirements | + +## Use Cases + +### Restricting Auth for Internal Plugins + +An internal plugin might only support specific authentication: + +```json +{ + "name": "internal_hr_system", + "allowedAuthTypes": ["oauth2"] +} +``` + +### Simple Public API Plugin + +A public weather API might need no authentication: + +```json +{ + "name": "public_weather", + "allowedAuthTypes": ["none", "api_key"] +} +``` + +## Frontend Integration + +The frontend can query auth types to: +1. Display only valid authentication options in plugin configuration UI +2. Validate user selections before saving +3. Show appropriate configuration fields based on auth type + +Example usage: + +```javascript +async function loadAuthTypes(pluginType) { + const response = await fetch(`/api/plugins/${pluginType}/auth-types`); + const data = await response.json(); + return data.allowedAuthTypes; +} +``` + +## Known Limitations + +- Auth types must be predefined in the schema +- Custom auth implementations require additional plugin code +- Definition files must be manually created for each plugin type + +## Related Features + +- Plugin Management +- Action/Plugin Registration +- OpenAPI Plugin Integration diff --git a/docs/explanation/features/v0.236.011/WEB_SEARCH_AZURE_AI_FOUNDRY.md b/docs/explanation/features/v0.236.011/WEB_SEARCH_AZURE_AI_FOUNDRY.md new file mode 100644 index 00000000..29d69114 --- /dev/null +++ b/docs/explanation/features/v0.236.011/WEB_SEARCH_AZURE_AI_FOUNDRY.md @@ -0,0 +1,119 @@ +# Web Search via Azure AI Foundry Agents + +## Overview + +SimpleChat now supports web search capability through Azure AI Foundry agents using the Grounding with Bing Search service. This feature enables AI responses to be augmented with real-time web search results, providing users with up-to-date information beyond the model's training data. + +**Version Implemented:** 0.236.011 + +## Key Features + +- **Azure AI Foundry Integration**: Leverages Azure AI Foundry's Grounding with Bing Search capability +- **Admin Consent Flow**: Requires explicit administrator consent before enabling due to data processing considerations +- **Activity Logging**: All consent acceptances are logged for compliance and audit purposes +- **Seamless Experience**: Web search results are automatically integrated into AI responses + +## Admin Consent Requirement + +Before web search can be enabled, administrators must acknowledge important data handling considerations: + +### Consent Message + +> When you use Grounding with Bing Search, your customer data is transferred outside of the Azure compliance boundary to the Grounding with Bing Search service. Grounding with Bing Search is not subject to the same data processing terms (including location of processing) and does not have the same compliance standards and certifications as the Azure AI Agent Service, as described in the Grounding with Bing Search TOU. + +### Why Consent is Required + +1. **Data Transfer**: Customer data is transferred outside the Azure compliance boundary +2. **Different Terms**: Grounding with Bing Search has different data processing terms +3. **Compliance Considerations**: Different compliance standards and certifications apply +4. **Organizational Responsibility**: Organizations must assess whether this meets their requirements + +## Configuration + +### Enabling Web Search + +1. Navigate to **Admin Settings** from the sidebar +2. Go to the **Search** or **Agents** section +3. Locate the **Web Search** toggle +4. Read and accept the consent message +5. Enable web search + +### Settings Stored + +| Setting | Description | +|---------|-------------| +| `enable_web_search` | Master toggle for web search capability | +| `web_search_consent_accepted` | Tracks whether consent has been accepted | + +## Technical Architecture + +### Backend Components + +| File | Purpose | +|------|---------| +| [route_frontend_admin_settings.py](../../../../application/single_app/route_frontend_admin_settings.py) | Handles consent flow and settings persistence | +| [functions_activity_logging.py](../../../../application/single_app/functions_activity_logging.py) | `log_web_search_consent_acceptance()` for audit logging | + +### Consent Flow Logic + +```python +# Simplified flow +web_search_consent_accepted = form_data.get('web_search_consent_accepted') == 'true' +requested_enable_web_search = form_data.get('enable_web_search') == 'on' +enable_web_search = requested_enable_web_search and web_search_consent_accepted + +# Log consent if newly accepted +if enable_web_search and web_search_consent_accepted and not settings.get('web_search_consent_accepted'): + log_web_search_consent_acceptance( + user_id=user_id, + admin_email=admin_email, + consent_text=web_search_consent_message, + source='admin_settings' + ) +``` + +### Activity Log Entry + +When consent is accepted, the following information is logged: +- Admin user ID +- Admin email address +- Full consent text +- Source of consent (admin_settings) +- Timestamp + +## User Experience + +### For End Users + +- Web search is transparent when enabled +- AI responses automatically incorporate relevant web search results +- Citations from web sources are displayed alongside responses + +### For Administrators + +- Clear consent flow before enabling +- One-time consent acceptance (persisted in settings) +- Audit trail of consent acceptance + +## Security Considerations + +1. **Consent Tracking**: All consent acceptances are logged for compliance +2. **Admin-Only Configuration**: Only administrators can enable web search +3. **Data Awareness**: Clear communication about data handling implications +4. **Revocability**: Web search can be disabled at any time + +## Related Features + +- [Azure AI Foundry Agent Support](AZURE_AI_FOUNDRY_AGENT_SUPPORT.md) +- Agent-based chat with real-time information + +## Dependencies + +- Azure AI Foundry account with Grounding with Bing Search enabled +- Proper Azure AI Foundry configuration in SimpleChat + +## Known Limitations + +- Web search results depend on Bing Search availability +- Results may vary based on Bing's index freshness +- Subject to Bing Search Terms of Use diff --git a/docs/explanation/fixes/AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md b/docs/explanation/fixes/v0.236.011/AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md similarity index 100% rename from docs/explanation/fixes/AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md rename to docs/explanation/fixes/v0.236.011/AGENT_PAYLOAD_FIELD_LENGTHS_FIX.md diff --git a/docs/explanation/fixes/AGENT_TEMPLATE_MAX_LENGTHS_FIX.md b/docs/explanation/fixes/v0.236.011/AGENT_TEMPLATE_MAX_LENGTHS_FIX.md similarity index 100% rename from docs/explanation/fixes/AGENT_TEMPLATE_MAX_LENGTHS_FIX.md rename to docs/explanation/fixes/v0.236.011/AGENT_TEMPLATE_MAX_LENGTHS_FIX.md diff --git a/docs/explanation/fixes/CONTROL_CENTER_DATE_LABELS_FIX.md b/docs/explanation/fixes/v0.236.011/CONTROL_CENTER_DATE_LABELS_FIX.md similarity index 100% rename from docs/explanation/fixes/CONTROL_CENTER_DATE_LABELS_FIX.md rename to docs/explanation/fixes/v0.236.011/CONTROL_CENTER_DATE_LABELS_FIX.md diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 6ea69868..4af7d241 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -36,9 +36,66 @@ * **Files Added**: `user-agreement.js` (frontend module), `route_backend_user_agreement.py` (API endpoints). * **Files Modified**: `admin_settings.html`, `route_frontend_admin_settings.py`, `base.html`, `_sidebar_nav.html`, `functions_activity_logging.py`, `workspace-documents.js`, `group_workspaces.html`, `public_workspace.js`, `chat-input-actions.js`. * (Ref: User Agreement modal, file upload workflows, activity logging, admin configuration) + +* **Web Search via Azure AI Foundry Agents** + * Web search capability through Azure AI Foundry agents using Grounding with Bing Search service. + * **Admin Consent Flow**: Requires explicit administrator consent before enabling due to data processing considerations outside Azure compliance boundary. + * **Consent Logging**: All consent acceptances are logged to activity logs for compliance and audit purposes. + * **Seamless Integration**: Web search results automatically integrated into AI responses when enabled. + * **Settings**: `enable_web_search` toggle and `web_search_consent_accepted` tracking in admin settings. + * **Files Modified**: `route_frontend_admin_settings.py`, `functions_activity_logging.py`. + * (Ref: Grounding with Bing Search, Azure AI Foundry, consent workflow, activity logging) + +* **Conversation Deep Linking** + * Direct URL links to specific conversations via query parameters for sharing and bookmarking. + * **URL Parameters**: Supports both `conversationId` and `conversation_id` query parameters. + * **Automatic URL Updates**: Current conversation ID automatically added to URL when selecting conversations. + * **Browser Integration**: Uses `history.replaceState()` for seamless URL updates without new history entries. + * **Error Handling**: Graceful handling of invalid or inaccessible conversation IDs with toast notifications. + * **Files Modified**: `chat-onload.js`, `chat-conversations.js`. + * (Ref: deep linking, URL parameters, conversation navigation, shareability) + +* **Agent Template Gallery** + * Curated collection of reusable agent configurations as starting points for new agents. + * **User Submission**: Users can submit their agents as templates for community use. + * **Admin Review Workflow**: Submitted templates require admin approval before publication (pending → approved → rejected workflow). + * **Rich Metadata**: Templates include titles, descriptions, helper text (140 char), tags, and predefined actions. + * **Configuration Options**: `enable_agent_template_gallery`, `agent_templates_allow_user_submission`, `agent_templates_require_approval`. + * **Database**: New `agent_templates` Cosmos DB container with partition key on `/id`. + * **Files Added**: `functions_agent_templates.py`. + * **Files Modified**: `config.py`, `route_frontend_admin_settings.py`. + * (Ref: agent templates, template gallery, admin approval workflow, reusable configurations) + +* **Plugin Authentication Type Constraints** + * Per-plugin-type authentication method restrictions for better security and API compatibility. + * **Schema-Based Defaults**: Falls back to global `AuthType` enum from `plugin.schema.json`. + * **Definition File Overrides**: Plugin-specific `.definition.json` files can restrict available auth types. + * **API Endpoint**: New `/api/plugins//auth-types` endpoint returns allowed auth types and source. + * **Frontend Integration**: UI can query allowed auth types to display only valid options. + * **Files Modified**: `route_backend_plugins.py`. + * (Ref: plugin authentication, auth type constraints, OpenAPI plugins, security) + +* **Azure AI Foundry Agent Support** + * First-class support for Azure AI Foundry agents as a native agent type. + * **Agent Type**: New `aifoundry` agent type in agent configuration. + * **Semantic Kernel Integration**: Uses Semantic Kernel's `AzureAIAgent` for execution. + * **Credential Support**: Supports Azure Default Credential and Client Secret authentication. + * **Citation & Model Tracking**: Captures citations and model information from Foundry agent responses. + * **Configuration**: Agent ID specified in `other_settings.azure_ai_foundry.agent_id`. + * **Files Added**: `foundry_agent_runtime.py`. + * **Files Modified**: `route_frontend_admin_settings.py`. + * (Ref: Azure AI Foundry, AzureAIAgent, Semantic Kernel, cloud-hosted agents) #### Bug Fixes +* **Control Center Chart Date Labels Fix** + * Fixed activity trends chart date labels to parse dates in local time instead of UTC. + * **Root Cause**: JavaScript `new Date()` was parsing date strings as UTC, causing labels to display previous day in western timezones. + * **Solution**: Parse date components explicitly and construct Date objects in local timezone. + * **Impact**: Chart x-axis labels now correctly show the intended dates regardless of user timezone. + * **Files Modified**: `control_center.html` (Chart.js date parsing logic). + * (Ref: Chart.js, date parsing, timezone handling, activity trends) + * **Sovereign Cloud Cognitive Services Scope Fix** * Fixed hardcoded commercial Azure cognitive services scope references that prevented authentication in Azure Government (MAG) and custom cloud environments. * **Root Cause**: `chat_stream_api` and `smart_http_plugin` used hardcoded commercial cognitive services scope URL instead of configurable value from `config.py`. From d0872e63cff9ab3c9f3f3dfcb39d017b466fa735 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 22 Jan 2026 16:06:22 -0500 Subject: [PATCH 2/9] Create release-notes-check.yml --- .github/workflows/release-notes-check.yml | 205 ++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 .github/workflows/release-notes-check.yml diff --git a/.github/workflows/release-notes-check.yml b/.github/workflows/release-notes-check.yml new file mode 100644 index 00000000..2eb7cee1 --- /dev/null +++ b/.github/workflows/release-notes-check.yml @@ -0,0 +1,205 @@ +name: Release Notes Check + +on: + pull_request: + branches: + - Development + types: + - opened + - reopened + - synchronize + - edited + +jobs: + check-release-notes: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files_yaml: | + code: + - 'application/single_app/**/*.py' + - 'application/single_app/**/*.js' + - 'application/single_app/**/*.html' + - 'application/single_app/**/*.css' + release_notes: + - 'docs/explanation/release_notes.md' + config: + - 'application/single_app/config.py' + + - name: Check for feature/fix keywords in PR + id: check-keywords + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "šŸ” Analyzing PR title and body for feature/fix indicators..." + + # Convert to lowercase for case-insensitive matching + title_lower=$(echo "$PR_TITLE" | tr '[:upper:]' '[:lower:]') + body_lower=$(echo "$PR_BODY" | tr '[:upper:]' '[:lower:]') + + # Check for feature indicators + if echo "$title_lower $body_lower" | grep -qE "(feat|feature|add|new|implement|introduce|enhancement|improve)"; then + echo "has_feature=true" >> $GITHUB_OUTPUT + echo "šŸ“¦ Feature-related keywords detected" + else + echo "has_feature=false" >> $GITHUB_OUTPUT + fi + + # Check for fix indicators + if echo "$title_lower $body_lower" | grep -qE "(fix|bug|patch|resolve|correct|repair|hotfix|issue)"; then + echo "has_fix=true" >> $GITHUB_OUTPUT + echo "šŸ› Fix-related keywords detected" + else + echo "has_fix=false" >> $GITHUB_OUTPUT + fi + + - name: Determine if release notes update is required + id: require-notes + env: + CODE_CHANGED: ${{ steps.changed-files.outputs.code_any_changed }} + CONFIG_CHANGED: ${{ steps.changed-files.outputs.config_any_changed }} + RELEASE_NOTES_CHANGED: ${{ steps.changed-files.outputs.release_notes_any_changed }} + HAS_FEATURE: ${{ steps.check-keywords.outputs.has_feature }} + HAS_FIX: ${{ steps.check-keywords.outputs.has_fix }} + run: | + echo "" + echo "================================" + echo "šŸ“‹ PR Analysis Summary" + echo "================================" + echo "Code files changed: $CODE_CHANGED" + echo "Config changed: $CONFIG_CHANGED" + echo "Release notes updated: $RELEASE_NOTES_CHANGED" + echo "Feature keywords found: $HAS_FEATURE" + echo "Fix keywords found: $HAS_FIX" + echo "================================" + echo "" + + # Determine if this PR likely needs release notes + needs_notes="false" + reason="" + + if [[ "$HAS_FEATURE" == "true" ]]; then + needs_notes="true" + reason="Feature-related keywords detected in PR title/body" + elif [[ "$HAS_FIX" == "true" ]]; then + needs_notes="true" + reason="Fix-related keywords detected in PR title/body" + elif [[ "$CODE_CHANGED" == "true" && "$CONFIG_CHANGED" == "true" ]]; then + needs_notes="true" + reason="Both code and config.py were modified" + fi + + echo "needs_notes=$needs_notes" >> $GITHUB_OUTPUT + echo "reason=$reason" >> $GITHUB_OUTPUT + + - name: Validate release notes update + env: + CODE_CHANGED: ${{ steps.changed-files.outputs.code_any_changed }} + RELEASE_NOTES_CHANGED: ${{ steps.changed-files.outputs.release_notes_any_changed }} + NEEDS_NOTES: ${{ steps.require-notes.outputs.needs_notes }} + REASON: ${{ steps.require-notes.outputs.reason }} + CODE_FILES: ${{ steps.changed-files.outputs.code_all_changed_files }} + run: | + echo "" + + if [[ "$NEEDS_NOTES" == "true" && "$RELEASE_NOTES_CHANGED" != "true" ]]; then + echo "āš ļø ==============================================" + echo "āš ļø RELEASE NOTES UPDATE RECOMMENDED" + echo "āš ļø ==============================================" + echo "" + echo "šŸ“ Reason: $REASON" + echo "" + echo "This PR appears to contain changes that should be documented" + echo "in the release notes (docs/explanation/release_notes.md)." + echo "" + echo "šŸ“ Code files changed:" + echo "$CODE_FILES" | tr ' ' '\n' | sed 's/^/ - /' + echo "" + echo "šŸ’” Please consider adding an entry to release_notes.md describing:" + echo " • New features added" + echo " • Bug fixes implemented" + echo " • Breaking changes (if any)" + echo " • Files modified" + echo "" + echo "šŸ“– Follow the existing format in release_notes.md" + echo "" + # Exit with warning (non-zero) to flag the PR but not block it + # Change 'exit 0' to 'exit 1' below to make this a hard requirement + exit 0 + elif [[ "$RELEASE_NOTES_CHANGED" == "true" ]]; then + echo "āœ… Release notes have been updated - great job!" + elif [[ "$CODE_CHANGED" != "true" ]]; then + echo "ā„¹ļø No significant code changes detected - release notes update not required." + else + echo "ā„¹ļø Changes appear to be minor - release notes update optional." + fi + + echo "" + echo "āœ… Release notes check completed successfully." + + - name: Post PR comment (when notes needed but missing) + if: steps.require-notes.outputs.needs_notes == 'true' && steps.changed-files.outputs.release_notes_any_changed != 'true' + uses: actions/github-script@v7 + with: + script: | + const reason = '${{ steps.require-notes.outputs.reason }}'; + + // Check if we already commented + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('šŸ“‹ Release Notes Reminder') + ); + + if (!botComment) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `## šŸ“‹ Release Notes Reminder + + This PR appears to contain changes that should be documented in the release notes. + + **Reason:** ${reason} + + ### šŸ“ Please consider updating: + \`docs/explanation/release_notes.md\` + + ### Template for new features: + \`\`\`markdown + * **Feature Name** + * Brief description of the feature. + * **Key Details**: Important implementation notes. + * **Files Modified**: \`file1.py\`, \`file2.js\`. + * (Ref: related components, patterns) + \`\`\` + + ### Template for bug fixes: + \`\`\`markdown + * **Bug Fix Title** + * Description of what was fixed. + * **Root Cause**: What caused the issue. + * **Solution**: How it was resolved. + * **Files Modified**: \`file.py\`. + * (Ref: related issue numbers, components) + \`\`\` + + --- + *This is an automated reminder. If this PR doesn't require release notes (e.g., internal refactoring, documentation-only changes), you can ignore this message.*` + }); + } From 4e0a54b3a5e7b19b018a868eb939731c36aaa994 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 22 Jan 2026 18:22:57 -0500 Subject: [PATCH 3/9] added debug_print logging to web search --- application/single_app/config.py | 2 +- application/single_app/route_backend_chats.py | 101 ++++++++++++++++-- .../single_app/static/images/custom_logo.png | Bin 11877 -> 11705 bytes .../static/images/custom_logo_dark.png | Bin 13468 -> 13770 bytes 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 622b5277..7c9c9bb2 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_chats.py b/application/single_app/route_backend_chats.py index e1e511a7..da7a5c72 100644 --- a/application/single_app/route_backend_chats.py +++ b/application/single_app/route_backend_chats.py @@ -4163,15 +4163,47 @@ def perform_web_search( agent_citations_list, web_search_citations_list, ): - if not settings.get("enable_web_search"): + debug_print("[WebSearch] ========== ENTERING perform_web_search ==========") + debug_print(f"[WebSearch] Parameters received:") + debug_print(f"[WebSearch] conversation_id: {conversation_id}") + debug_print(f"[WebSearch] user_id: {user_id}") + debug_print(f"[WebSearch] user_message: {user_message[:100] if user_message else None}...") + debug_print(f"[WebSearch] user_message_id: {user_message_id}") + debug_print(f"[WebSearch] chat_type: {chat_type}") + debug_print(f"[WebSearch] document_scope: {document_scope}") + debug_print(f"[WebSearch] active_group_id: {active_group_id}") + debug_print(f"[WebSearch] active_public_workspace_id: {active_public_workspace_id}") + debug_print(f"[WebSearch] search_query: {search_query[:100] if search_query else None}...") + + enable_web_search = settings.get("enable_web_search") + debug_print(f"[WebSearch] enable_web_search setting: {enable_web_search}") + + if not enable_web_search: + debug_print("[WebSearch] Web search is DISABLED in settings, returning early") return + debug_print("[WebSearch] Web search is ENABLED, proceeding...") + web_search_agent = settings.get("web_search_agent") or {} - foundry_settings = ( - (web_search_agent.get("other_settings") or {}).get("azure_ai_foundry") or {} - ) + debug_print(f"[WebSearch] web_search_agent config present: {bool(web_search_agent)}") + if web_search_agent: + # Avoid logging sensitive data, just log structure + debug_print(f"[WebSearch] web_search_agent keys: {list(web_search_agent.keys())}") + + other_settings = web_search_agent.get("other_settings") or {} + debug_print(f"[WebSearch] other_settings keys: {list(other_settings.keys()) if other_settings else ''}") + + foundry_settings = other_settings.get("azure_ai_foundry") or {} + debug_print(f"[WebSearch] foundry_settings present: {bool(foundry_settings)}") + if foundry_settings: + # Log only non-sensitive keys + safe_keys = ['agent_id', 'project_id', 'endpoint'] + safe_info = {k: foundry_settings.get(k, '') for k in safe_keys} + debug_print(f"[WebSearch] foundry_settings (safe keys): {safe_info}") agent_id = (foundry_settings.get("agent_id") or "").strip() + debug_print(f"[WebSearch] Extracted agent_id: '{agent_id}'") + if not agent_id: log_event( "[WebSearch] Skipping Foundry web search: agent_id is not configured", @@ -4181,16 +4213,24 @@ def perform_web_search( }, level=logging.WARNING, ) + debug_print("[WebSearch] Foundry agent_id not configured, skipping web search.") return + debug_print(f"[WebSearch] Agent ID is configured: {agent_id}") + query_text = None try: query_text = search_query + debug_print(f"[WebSearch] Using search_query as query_text: {query_text[:100] if query_text else None}...") except NameError: query_text = None + debug_print("[WebSearch] search_query not defined, query_text is None") query_text = (query_text or user_message or "").strip() + debug_print(f"[WebSearch] Final query_text after fallback: '{query_text[:100] if query_text else ''}'") + if not query_text: + debug_print("[WebSearch] Query text is EMPTY after processing, skipping web search") log_event( "[WebSearch] Skipping Foundry web search: empty query", extra={ @@ -4201,9 +4241,11 @@ def perform_web_search( ) return + debug_print(f"[WebSearch] Building message history with query: {query_text[:100]}...") message_history = [ ChatMessageContent(role="user", content=query_text) ] + debug_print(f"[WebSearch] Message history created with {len(message_history)} message(s)") try: foundry_metadata = { @@ -4216,7 +4258,12 @@ def perform_web_search( "public_workspace_id": active_public_workspace_id, "search_query": query_text, } - + debug_print(f"[WebSearch] Foundry metadata prepared: {json.dumps(foundry_metadata, default=str)}") + + debug_print("[WebSearch] Calling execute_foundry_agent...") + debug_print(f"[WebSearch] foundry_settings keys: {list(foundry_settings.keys())}") + debug_print(f"[WebSearch] global_settings type: {type(settings)}") + result = asyncio.run( execute_foundry_agent( foundry_settings=foundry_settings, @@ -4250,6 +4297,26 @@ def perform_web_search( ) return + debug_print("[WebSearch] ========== FOUNDRY AGENT RESULT ==========") + debug_print(f"[WebSearch] Result type: {type(result)}") + debug_print(f"[WebSearch] Result has message: {bool(result.message)}") + debug_print(f"[WebSearch] Result has citations: {bool(result.citations)}") + debug_print(f"[WebSearch] Result has metadata: {bool(result.metadata)}") + debug_print(f"[WebSearch] Result model: {getattr(result, 'model', 'N/A')}") + + if result.message: + debug_print(f"[WebSearch] Result message length: {len(result.message)} chars") + debug_print(f"[WebSearch] Result message preview: {result.message[:500] if len(result.message) > 500 else result.message}") + else: + debug_print("[WebSearch] Result message is EMPTY or None") + + if result.citations: + debug_print(f"[WebSearch] Result citations count: {len(result.citations)}") + for i, cit in enumerate(result.citations[:3]): + debug_print(f"[WebSearch] Citation {i}: {json.dumps(cit, default=str)[:200]}...") + else: + debug_print("[WebSearch] Result citations is EMPTY or None") + if result.metadata: try: metadata_payload = json.dumps(result.metadata, default=str) @@ -4260,23 +4327,35 @@ def perform_web_search( debug_print("[WebSearch] Foundry metadata: ") if result.message: + debug_print("[WebSearch] Adding result message to system_messages_for_augmentation") system_messages_for_augmentation.append({ "role": "system", "content": f"Web search results:\n{result.message}", }) + debug_print(f"[WebSearch] Added system message to augmentation list. Total augmentation messages: {len(system_messages_for_augmentation)}") + debug_print("[WebSearch] Extracting web citations from result message...") web_citations = _extract_web_search_citations_from_content(result.message) + debug_print(f"[WebSearch] Extracted {len(web_citations)} web citations from message content") if web_citations: web_search_citations_list.extend(web_citations) + debug_print(f"[WebSearch] Total web_search_citations_list now has {len(web_search_citations_list)} citations") + else: + debug_print("[WebSearch] No web citations extracted from message content") + else: + debug_print("[WebSearch] No result.message to process for augmentation") citations = result.citations or [] + debug_print(f"[WebSearch] Processing {len(citations)} citations from result.citations") if citations: - for citation in citations: + for i, citation in enumerate(citations): + debug_print(f"[WebSearch] Processing citation {i}: {json.dumps(citation, default=str)[:200]}...") try: serializable = json.loads(json.dumps(citation, default=str)) except (TypeError, ValueError): serializable = {"value": str(citation)} citation_title = serializable.get("title") or serializable.get("url") or "Web search source" + debug_print(f"[WebSearch] Adding agent citation with title: {citation_title}") agent_citations_list.append({ "tool_name": citation_title, "function_name": "azure_ai_foundry_web_search", @@ -4286,6 +4365,9 @@ def perform_web_search( "timestamp": datetime.utcnow().isoformat(), "success": True, }) + debug_print(f"[WebSearch] Total agent_citations_list now has {len(agent_citations_list)} citations") + else: + debug_print("[WebSearch] No citations in result.citations to process") debug_print(f"[WebSearch] Starting token usage extraction from Foundry metadata. Metadata: {result.metadata}") token_usage = _extract_token_usage_from_metadata(result.metadata or {}) @@ -4326,6 +4408,13 @@ def perform_web_search( level=logging.WARNING, ) + debug_print("[WebSearch] ========== FINAL SUMMARY ==========") + debug_print(f"[WebSearch] system_messages_for_augmentation count: {len(system_messages_for_augmentation)}") + debug_print(f"[WebSearch] agent_citations_list count: {len(agent_citations_list)}") + debug_print(f"[WebSearch] web_search_citations_list count: {len(web_search_citations_list)}") + debug_print(f"[WebSearch] Token usage extracted: {token_usage}") + debug_print("[WebSearch] ========== EXITING perform_web_search ==========") + log_event( "[WebSearch] Foundry web search invocation complete", extra={ diff --git a/application/single_app/static/images/custom_logo.png b/application/single_app/static/images/custom_logo.png index ecf6e6521a737af56bcc82321caff1acefb63494..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 11877 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 F{sTGwC=37q diff --git a/application/single_app/static/images/custom_logo_dark.png b/application/single_app/static/images/custom_logo_dark.png index 4f28194576a32f4463ff13ac96521f979e739bb0..b3beb694201dc8b371e45c973895a95e211eab8e 100644 GIT binary patch literal 13770 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 literal 13468 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 aj13{1xX1-4^7hveoPvysbhV^u=>Gr|kZNTB From c44ca653e79c93b4ac99f362f0d47685df03377d Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Fri, 23 Jan 2026 07:52:31 -0500 Subject: [PATCH 4/9] Updated the admin UI for web search --- application/single_app/config.py | 2 +- application/single_app/functions_settings.py | 2 +- application/single_app/route_backend_chats.py | 29 +- .../single_app/templates/_agent_modal.html | 2 +- .../templates/_web_search_foundry_info.html | 414 ++++++++++++++++++ .../single_app/templates/admin_settings.html | 20 +- ...EB_SEARCH_FAILURE_GRACEFUL_HANDLING_FIX.md | 184 ++++++++ docs/explanation/release_notes.md | 8 +- .../test_web_search_failure_handling.py | 276 ++++++++++++ 9 files changed, 915 insertions(+), 22 deletions(-) create mode 100644 application/single_app/templates/_web_search_foundry_info.html create mode 100644 docs/explanation/fixes/v0.236.011/WEB_SEARCH_FAILURE_GRACEFUL_HANDLING_FIX.md create mode 100644 functional_tests/test_web_search_failure_handling.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 7c9c9bb2..f8a46286 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.012" +VERSION = "0.236.014" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 86c31666..83f96dcb 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -234,7 +234,7 @@ def get_settings(use_cosmos=False): 'azure_ai_foundry': { 'agent_id': '', 'endpoint': '', - 'api_version': '', + 'api_version': 'v1', 'authentication_type': 'managed_identity', 'managed_identity_type': 'system_assigned', 'managed_identity_client_id': '', diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py index da7a5c72..ad514e6f 100644 --- a/application/single_app/route_backend_chats.py +++ b/application/single_app/route_backend_chats.py @@ -4180,7 +4180,7 @@ def perform_web_search( if not enable_web_search: debug_print("[WebSearch] Web search is DISABLED in settings, returning early") - return + return True # Not an error, just disabled debug_print("[WebSearch] Web search is ENABLED, proceeding...") @@ -4214,7 +4214,12 @@ def perform_web_search( level=logging.WARNING, ) debug_print("[WebSearch] Foundry agent_id not configured, skipping web search.") - return + # Add failure message so the model knows search was requested but not configured + system_messages_for_augmentation.append({ + "role": "system", + "content": "Web search was requested but is not properly configured. Please inform the user that web search is currently unavailable and you cannot provide real-time information. Do not attempt to answer questions requiring current information from your training data.", + }) + return False # Configuration error debug_print(f"[WebSearch] Agent ID is configured: {agent_id}") @@ -4239,7 +4244,7 @@ def perform_web_search( }, level=logging.WARNING, ) - return + return True # Not an error, just empty query debug_print(f"[WebSearch] Building message history with query: {query_text[:100]}...") message_history = [ @@ -4283,7 +4288,12 @@ def perform_web_search( level=logging.ERROR, exceptionTraceback=True, ) - return + # Add failure message so the model informs the user + system_messages_for_augmentation.append({ + "role": "system", + "content": f"Web search failed with error: {exc}. Please inform the user that the web search encountered an error and you cannot provide real-time information for this query. Do not attempt to answer questions requiring current information from your training data - instead, acknowledge the search failure and suggest the user try again.", + }) + return False # Search failed except Exception as exc: log_event( f"[WebSearch] Unexpected error invoking Foundry agent: {exc}", @@ -4295,7 +4305,12 @@ def perform_web_search( level=logging.ERROR, exceptionTraceback=True, ) - return + # Add failure message so the model informs the user + system_messages_for_augmentation.append({ + "role": "system", + "content": f"Web search failed with an unexpected error: {exc}. Please inform the user that the web search encountered an error and you cannot provide real-time information for this query. Do not attempt to answer questions requiring current information from your training data - instead, acknowledge the search failure and suggest the user try again.", + }) + return False # Search failed debug_print("[WebSearch] ========== FOUNDRY AGENT RESULT ==========") debug_print(f"[WebSearch] Result type: {type(result)}") @@ -4424,4 +4439,6 @@ def perform_web_search( "citation_count": len(citations), }, level=logging.INFO, - ) \ No newline at end of file + ) + + return True # Search succeeded \ No newline at end of file diff --git a/application/single_app/templates/_agent_modal.html b/application/single_app/templates/_agent_modal.html index a90260ec..80f068ca 100644 --- a/application/single_app/templates/_agent_modal.html +++ b/application/single_app/templates/_agent_modal.html @@ -94,7 +94,7 @@
Model & Connection
- +
diff --git a/application/single_app/templates/_web_search_foundry_info.html b/application/single_app/templates/_web_search_foundry_info.html new file mode 100644 index 00000000..f9226aae --- /dev/null +++ b/application/single_app/templates/_web_search_foundry_info.html @@ -0,0 +1,414 @@ + + + + diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 541b2bac..d0a91bef 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -3152,7 +3152,12 @@

-
Web Search (Azure AI Foundry Agent)
+
+
Web Search (Azure AI Foundry Agent)
+ +

Enable web search by routing queries through an Azure AI Foundry agent configured by admins.

Web Search (Azure AI Foundry Agent)
> - -
-
- - -
-
@@ -4110,6 +4105,9 @@