Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
4e4f105
Add custom subdomain support for OpenAI and Speech Service in Terraform
Jan 2, 2026
be56540
Merge branch 'Development' of https://github.com/vivche/simplechat-de…
Jan 7, 2026
e13ba0c
Merge remote-tracking branch 'upstream/Development' into Development
Jan 23, 2026
087fb3d
feat: Add ServiceNow integration documentation and bug fixes
Jan 23, 2026
502355f
Removed the readme files for bug fix details
Jan 24, 2026
33bee68
Updated servicenow integration readme
Jan 24, 2026
cd8c520
chore: Revert custom logo changes to upstream version
Jan 24, 2026
fb8181b
chore: Revert terraform main.tf to upstream version
Jan 24, 2026
660d76c
Removed the two openai sample spec downloaed from servicennow site
Jan 24, 2026
de866eb
Update docs/how-to/agents/ServiceNow/servicenow_agent_instructions.txt
vivche Jan 24, 2026
20f994a
Update docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow…
vivche Jan 24, 2026
351f143
Update docs/how-to/azure_speech_managed_identity_manul_setup.md
vivche Jan 24, 2026
5fe5b14
Update application/single_app/semantic_kernel_plugins/openapi_plugin_…
vivche Jan 24, 2026
9626219
Checked in the bug fix detail readme to docs/explanation/fixes/v0.236…
Jan 24, 2026
6364827
Merge branch 'servicenow-integration' of https://github.com/vivche/si…
Jan 24, 2026
dce54a1
Added version number to the feature readme files
Jan 24, 2026
8353d77
Added version number to document, and removed redudant import statement
Jan 24, 2026
5aa7007
refactor: use _ for intentionally unused variable in AI Search test
Jan 24, 2026
548d8d8
Removed azure_speech_managed_indeity_manual readme file since it is u…
Jan 24, 2026
62b0b5b
update version numbers to 0.236.012 in bug fix documentation
Jan 24, 2026
b0be501
Update application/single_app/semantic_kernel_loader.py
vivche Jan 24, 2026
e264a13
Update docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowle…
vivche Jan 24, 2026
b04bd67
Update docs/explanation/fixes/v0.236.012/AZURE_AI_SEARCH_TEST_CONNECT…
vivche Jan 24, 2026
84c01e9
Update docs/how-to/agents/ServiceNow/open_api_specs/sample_now_knowle…
vivche Jan 24, 2026
c8e383e
Update docs/how-to/agents/ServiceNow/open_api_specs/sample_servicenow…
vivche Jan 24, 2026
39dc7a4
Update docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md
vivche Jan 24, 2026
2e8c737
Remvoed debug statements that might include senstive info
Jan 24, 2026
d0581d5
Merge branch 'servicenow-integration' of https://github.com/vivche/si…
Jan 24, 2026
0c23a78
Rollback Azure AI Search test connection fix for separate PR
Jan 24, 2026
7f8248a
Update application/single_app/semantic_kernel_plugins/openapi_plugin_…
vivche Jan 24, 2026
a0fbffd
Update docs/explanation/fixes/v0.236.012/GROUP_AGENT_LOADING_FIX.md
vivche Jan 24, 2026
4bac07a
Update docs/explanation/fixes/v0.236.012/GROUP_ACTION_OAUTH_SCHEMA_ME…
vivche Jan 24, 2026
1c31db4
Update docs/how-to/agents/ServiceNow/SERVICENOW_OAUTH_SETUP.md
vivche Jan 24, 2026
7e0c688
Fix Azure AI Search test connection with managed identity
Jan 24, 2026
6b0164a
Fix Azure AI Search test connection with managed identity
Jan 24, 2026
c910ede
Corrected file folder name
Jan 24, 2026
f188224
Merge branch 'ai-search-test-connection-fix' of https://github.com/vi…
Jan 24, 2026
8ae8518
Corrected the version number to reference 0.236.012
Jan 24, 2026
a82ecb7
Removed unneeded folder and document
Jan 24, 2026
589291b
Revert terraform main.tf to upstream/Development version
Jan 24, 2026
d017028
updated the logging logic when running retention delete with archivin…
paullizer Jan 24, 2026
2e8e87a
Corrected version to 0.236.011 (#645)
paullizer Jan 26, 2026
6042461
v0.237.001 (#649)
paullizer Jan 26, 2026
9c698af
Merge branch 'Staging' into Development
paullizer Jan 26, 2026
84e00cb
Use Microsoft python base image
clarked-msft Jan 26, 2026
317c6ee
Add python ENV vars
clarked-msft Jan 26, 2026
25f41fb
Add python ENV vars
clarked-msft Jan 26, 2026
0753f52
Install deps to systme
clarked-msft Jan 26, 2026
f2958f0
Add temp dir to image and pip conf support
clarked-msft Jan 26, 2026
efd6fe7
Add custom-ca-certificates dir
clarked-msft Jan 26, 2026
231b792
Merge pull request #653 from clarked-msft/msft-python-image
Bionic711 Jan 26, 2026
7d0a792
Logo bug fix (#654)
paullizer Jan 26, 2026
1cdb27a
Merge branch 'Staging' into Development
paullizer Jan 26, 2026
823e6fa
Rentention policy (#657)
paullizer Jan 26, 2026
1c1c845
Merge branch 'Staging' into Development
paullizer Jan 26, 2026
31e8341
Added ServiceNow support for create and publish article. Including r…
Jan 27, 2026
c70db6b
Replace actual servicenow instance name with generic name in the read…
Jan 27, 2026
fa72a65
Merge remote-tracking branch 'upstream/Development' into servicenow-i…
Jan 27, 2026
0ed07b1
Changed version number in ServiceNow readme files to 0.237.005 since …
Jan 27, 2026
61d8a8b
Enhance ServiceNow agent for managing new KB article creation
Jan 28, 2026
715bb6b
Added readme and open ai specs and agent instructions to support Serv…
Jan 28, 2026
b5804ad
Remove any references to actual ServiceNow instances
Jan 28, 2026
6c4b14e
Merge upstream/Development into ai-search-test-connection-fix
Jan 28, 2026
7ace8b7
Merge pull request #641 from vivche/ai-search-test-connection-fix
Bionic711 Jan 29, 2026
05a14e4
fixed retention policy runtime bug and sidebar bug (#672)
paullizer Jan 30, 2026
5cd0f3b
Merge branch 'Staging' into Development
paullizer Jan 30, 2026
0e0c437
Fix: Windows Unicode encoding issue for video uploads (#662)
vivche Jan 30, 2026
ab456a4
Update docs/how-to/azure_speech_managed_identity_manul_setup.md (#675)
paullizer Jan 30, 2026
534eb72
Add custom subdomain support for OpenAI and Speech Service in Terrafo…
vivche Jan 30, 2026
251b949
0.237.006 (#676)
paullizer Jan 30, 2026
cd09c7a
docs: Update release notes for ServiceNow integration and bug fixes
Jan 30, 2026
634040d
Update release_notes.md
paullizer Jan 30, 2026
944b581
Merge branch 'Staging' into Development
paullizer Jan 30, 2026
39e812c
resolve conflict
Jan 30, 2026
0da710a
fixed sidebar race condition (#679)
paullizer Jan 30, 2026
1e4b524
Merge branch 'Staging' into Development
paullizer Jan 30, 2026
fd66132
Merge branch 'Development' into servicenow-integration, and moved fix…
Jan 31, 2026
0c161ae
fix the version number in config.py
Jan 31, 2026
bf90baf
Security: Restrict group agent loading to active group only
Jan 31, 2026
8a9ad98
Fixed an instruction error that caused semantic kernel to fall back t…
Jan 31, 2026
28a557e
Merge pull request #640 from vivche/servicenow-integration
Bionic711 Jan 31, 2026
82f8a89
Fixed! The issue was caused by duplicated code blocks (#683)
paullizer Feb 3, 2026
4c24cc8
Manage group frontend bug (#684)
paullizer Feb 3, 2026
b66bee9
initial feature add
paullizer Feb 4, 2026
ef5f468
added tag endpoint
paullizer Feb 4, 2026
af15c05
return unused tags too
paullizer Feb 4, 2026
ef14203
Bicepfix (#690)
eldong Feb 5, 2026
296d987
working on ui
paullizer Feb 5, 2026
a4a4224
Search bug fix 20260229 (#697)
paullizer Feb 9, 2026
541dd60
Overhauled and updated file extension definition & MAG audio file tra…
Xeelee33 Feb 9, 2026
7c2eb0c
Update release_notes.md (#698)
paullizer Feb 9, 2026
e102efe
Merge branch 'Staging' into Development
paullizer Feb 9, 2026
2429489
removed duplicate code causing bugs (#701)
paullizer Feb 9, 2026
b529d60
Chat file upload error (#709)
paullizer Feb 11, 2026
0cd14b9
Merge branch 'Staging' into Development
paullizer Feb 11, 2026
7381359
edit folders
paullizer Feb 18, 2026
66f9287
sorting fixed
paullizer Feb 18, 2026
654739a
chat with working with personal
paullizer Feb 19, 2026
3daf815
tagging added to groups and public workspaces
paullizer Feb 20, 2026
e317139
added tags to group and public workspace
paullizer Feb 22, 2026
6d9e6b0
fixed tags in group and public workspaces
paullizer Feb 23, 2026
978ca4f
Update .gitignore
paullizer Feb 23, 2026
de3a523
fixed citation bug
paullizer Feb 23, 2026
4befb2c
cleaned up workspaces
paullizer Feb 23, 2026
47d07da
Bugfix/globalagentscreation (#720)
Bionic711 Feb 23, 2026
f36f990
Add Dockerfile and update devcontainer configuration for Python envir…
SteveCInVA Feb 23, 2026
dc746a4
extended document dropdown width
paullizer Feb 23, 2026
b256a07
Update chat-documents.js
paullizer Feb 23, 2026
b74c077
Create CLAUDE.md
paullizer Feb 23, 2026
8168bb1
added features and updated release notes
paullizer Feb 23, 2026
59e8297
Added ability for admin to disable/enable workspace lock
paullizer Feb 23, 2026
deae525
Merge branch 'Development' into workspace-folders
paullizer Feb 23, 2026
e0da096
Add tags to blob metadata when enhanced citations is enabled
paullizer Feb 23, 2026
edcce6d
Merge branch 'workspace-folders' of https://github.com/microsoft/simp…
paullizer Feb 23, 2026
f78e8a5
Fix/agent action 500 error (#731)
paullizer Feb 23, 2026
b92ec5a
Update release_notes.md
paullizer Feb 23, 2026
a924a30
Merge branch 'workspace-folders' into Development
paullizer Feb 23, 2026
a334377
fixed active workspace bug for users switching public workspaces
paullizer Feb 24, 2026
3100d85
optional only allow owner to create/edit/delete group agents and actions
paullizer Feb 24, 2026
d3667b2
Merge pull request #736 from microsoft/feature/owner-required-to-crea…
Bionic711 Feb 24, 2026
9e29f22
Added retention policy UI for groups and public workspaces (#730)
eldong Feb 24, 2026
0ea45c2
fixed showToast
paullizer Feb 25, 2026
0bd9f12
Hardened `get_user_settings()` to normalize malformed or missing `set…
paullizer Feb 26, 2026
df8964f
fixed potential sql injection with new tags feature
paullizer Mar 2, 2026
941a4d2
Merge pull request #751 from microsoft/fix/tag-sql-injection
Bionic711 Mar 2, 2026
f10997d
Merge branch 'Development' into fix-harden-get-user-settings
Bionic711 Mar 2, 2026
3d7e8d4
Merge pull request #744 from microsoft/fix-harden-get-user-settings
Bionic711 Mar 2, 2026
3acf5c2
Merge branch 'Development' into fix-public-workspace-active-bug
Bionic711 Mar 2, 2026
834bfc4
Apply suggestion from @Copilot
paullizer Mar 2, 2026
ca93510
fix js location
paullizer Mar 2, 2026
d3fcf85
Merge branch 'fix-public-workspace-active-bug' of https://github.com/…
paullizer Mar 2, 2026
433a7c3
update wrapper (#741)
Bionic711 Mar 2, 2026
b64c9e8
Merge pull request #735 from microsoft/fix-public-workspace-active-bug
Bionic711 Mar 2, 2026
271a338
Merge branch 'Staging' into Development
nadoylemsft Mar 2, 2026
f7e1e41
Added ability to export conversations & set retention policies (#734)
eldong Mar 3, 2026
12a88ca
Merge branch 'Staging' into Development
paullizer Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from route_backend_public_documents import *
from route_backend_public_prompts import *
from route_backend_user_agreement import register_route_backend_user_agreement
from route_backend_conversation_export import register_route_backend_conversation_export
from route_backend_speech import register_route_backend_speech
from route_backend_tts import register_route_backend_tts
from route_enhanced_citations import register_enhanced_citations_routes
Expand Down Expand Up @@ -628,6 +629,9 @@ def list_semantic_kernel_plugins():
# ------------------- API Public Workspaces Routes -------
register_route_backend_public_workspaces(app)

# ------------------- API Conversation Export Routes -----
register_route_backend_conversation_export(app)

# ------------------- API Public Documents Routes --------
register_route_backend_public_documents(app)

Expand Down
6 changes: 6 additions & 0 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
import fitz # PyMuPDF
import math
import mimetypes
# Register font MIME types so Flask serves them correctly (required for
# X-Content-Type-Options: nosniff to not block Bootstrap Icons)
mimetypes.add_type('font/woff', '.woff')
mimetypes.add_type('font/woff2', '.woff2')
mimetypes.add_type('font/ttf', '.ttf')
mimetypes.add_type('font/otf', '.otf')
import openpyxl
import xlrd
import traceback
Expand Down
288 changes: 288 additions & 0 deletions application/single_app/route_backend_conversation_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# route_backend_conversation_export.py

import io
import json
import zipfile
from datetime import datetime

from config import *
from functions_authentication import *
from functions_settings import *
from flask import Response, jsonify, request, make_response
from functions_debug import debug_print
from swagger_wrapper import swagger_route, get_auth_security


def register_route_backend_conversation_export(app):
"""Register conversation export API routes."""

@app.route('/api/conversations/export', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_export_conversations():
"""
Export one or more conversations in JSON or Markdown format.
Supports single-file or ZIP packaging.
Request body:
conversation_ids (list): List of conversation IDs to export.
format (str): Export format — "json" or "markdown".
packaging (str): Output packaging — "single" or "zip".
"""
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

data = request.get_json()
if not data:
return jsonify({'error': 'Request body is required'}), 400

conversation_ids = data.get('conversation_ids', [])
export_format = data.get('format', 'json').lower()
packaging = data.get('packaging', 'single').lower()

if not conversation_ids or not isinstance(conversation_ids, list):
return jsonify({'error': 'At least one conversation_id is required'}), 400

if export_format not in ('json', 'markdown'):
return jsonify({'error': 'Format must be "json" or "markdown"'}), 400

if packaging not in ('single', 'zip'):
return jsonify({'error': 'Packaging must be "single" or "zip"'}), 400

try:
exported = []
for conv_id in conversation_ids:
# Verify ownership and fetch conversation
try:
conversation = cosmos_conversations_container.read_item(
item=conv_id,
partition_key=conv_id
)
except Exception:
debug_print(f"Export: conversation {conv_id} not found or access denied")
continue

# Verify user owns this conversation
if conversation.get('user_id') != user_id:
debug_print(f"Export: user {user_id} does not own conversation {conv_id}")
continue

# Fetch messages ordered by timestamp
message_query = f"""
SELECT * FROM c
WHERE c.conversation_id = '{conv_id}'
ORDER BY c.timestamp ASC
"""
messages = list(cosmos_messages_container.query_items(
query=message_query,
partition_key=conv_id
))

# Filter for active thread messages only
filtered_messages = []
for msg in messages:
thread_info = msg.get('metadata', {}).get('thread_info', {})
active = thread_info.get('active_thread')
if active is True or active is None or 'active_thread' not in thread_info:
filtered_messages.append(msg)

exported.append({
'conversation': _sanitize_conversation(conversation),
'messages': [_sanitize_message(m) for m in filtered_messages]
})

if not exported:
return jsonify({'error': 'No accessible conversations found'}), 404

# Generate export content
timestamp_str = datetime.utcnow().strftime('%Y%m%d_%H%M%S')

if packaging == 'zip':
return _build_zip_response(exported, export_format, timestamp_str)
else:
return _build_single_file_response(exported, export_format, timestamp_str)

except Exception as e:
debug_print(f"Export error: {str(e)}")
return jsonify({'error': f'Export failed: {str(e)}'}), 500

def _sanitize_conversation(conv):
"""Return only user-facing conversation fields."""
return {
'id': conv.get('id'),
'title': conv.get('title', 'Untitled'),
'last_updated': conv.get('last_updated', ''),
'chat_type': conv.get('chat_type', 'personal'),
'tags': conv.get('tags', []),
'is_pinned': conv.get('is_pinned', False),
'context': conv.get('context', [])
}

def _sanitize_message(msg):
"""Return only user-facing message fields."""
result = {
'role': msg.get('role', ''),
'content': msg.get('content', ''),
'timestamp': msg.get('timestamp', ''),
}
# Include citations if present
if msg.get('citations'):
result['citations'] = msg['citations']
# Include context/tool info if present
if msg.get('context'):
result['context'] = msg['context']
return result

def _build_single_file_response(exported, export_format, timestamp_str):
"""Build a single-file download response."""
if export_format == 'json':
content = json.dumps(exported, indent=2, ensure_ascii=False, default=str)
filename = f"conversations_export_{timestamp_str}.json"
content_type = 'application/json; charset=utf-8'
else:
parts = []
for entry in exported:
parts.append(_conversation_to_markdown(entry))
content = '\n\n---\n\n'.join(parts)
filename = f"conversations_export_{timestamp_str}.md"
content_type = 'text/markdown; charset=utf-8'

response = make_response(content)
response.headers['Content-Type'] = content_type
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
return response

def _build_zip_response(exported, export_format, timestamp_str):
"""Build a ZIP archive containing one file per conversation."""
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for entry in exported:
conv = entry['conversation']
safe_title = _safe_filename(conv.get('title', 'Untitled'))
conv_id_short = conv.get('id', 'unknown')[:8]

if export_format == 'json':
file_content = json.dumps(entry, indent=2, ensure_ascii=False, default=str)
ext = 'json'
else:
file_content = _conversation_to_markdown(entry)
ext = 'md'

file_name = f"{safe_title}_{conv_id_short}.{ext}"
zf.writestr(file_name, file_content)

buffer.seek(0)
filename = f"conversations_export_{timestamp_str}.zip"

response = make_response(buffer.read())
response.headers['Content-Type'] = 'application/zip'
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
return response

def _conversation_to_markdown(entry):
"""Convert a conversation + messages entry to Markdown format."""
conv = entry['conversation']
messages = entry['messages']

lines = []
title = conv.get('title', 'Untitled')
lines.append(f"# {title}")
lines.append('')

# Metadata
last_updated = conv.get('last_updated', '')
chat_type = conv.get('chat_type', 'personal')
tags = conv.get('tags', [])

lines.append(f"**Last Updated:** {last_updated} ")
lines.append(f"**Chat Type:** {chat_type} ")
if tags:
tag_strs = [str(t) for t in tags]
lines.append(f"**Tags:** {', '.join(tag_strs)} ")
lines.append(f"**Messages:** {len(messages)} ")
lines.append('')
lines.append('---')
lines.append('')

# Messages
for msg in messages:
role = msg.get('role', 'unknown')
timestamp = msg.get('timestamp', '')
raw_content = msg.get('content', '')
content = _normalize_content(raw_content)

role_label = role.capitalize()
if role == 'assistant':
role_label = 'Assistant'
elif role == 'user':
role_label = 'User'
elif role == 'system':
role_label = 'System'
elif role == 'tool':
role_label = 'Tool'

lines.append(f"### {role_label}")
if timestamp:
lines.append(f"*{timestamp}*")
lines.append('')
lines.append(content)
lines.append('')

# Citations
citations = msg.get('citations')
if citations:
lines.append('**Citations:**')
if isinstance(citations, list):
for cit in citations:
if isinstance(cit, dict):
source = cit.get('title') or cit.get('filepath') or cit.get('url', 'Unknown')
lines.append(f"- {source}")
else:
lines.append(f"- {cit}")
lines.append('')

lines.append('---')
lines.append('')

return '\n'.join(lines)

def _normalize_content(content):
"""Normalize message content to a plain string.
Content may be a string, a list of content-part dicts
(e.g. [{"type": "text", "text": "..."}, ...]), or a dict.
"""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict):
if item.get('type') == 'text':
parts.append(item.get('text', ''))
elif item.get('type') == 'image_url':
parts.append('[Image]')
else:
parts.append(str(item))
else:
parts.append(str(item))
return '\n'.join(parts)
if isinstance(content, dict):
if content.get('type') == 'text':
return content.get('text', '')
return str(content)
return str(content) if content else ''

def _safe_filename(title):
"""Create a filesystem-safe filename from a conversation title."""
import re
# Remove or replace unsafe characters
safe = re.sub(r'[<>:"/\\|?*]', '_', title)
safe = re.sub(r'\s+', '_', safe)
safe = safe.strip('_. ')
# Truncate to reasonable length
if len(safe) > 50:
safe = safe[:50]
return safe or 'Untitled'
16 changes: 16 additions & 0 deletions application/single_app/static/css/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,22 @@ body.sidebar-nav-enabled.has-classification-banner .container-fluid {
#conversations-actions {
opacity: 1;
transition: opacity 0.2s ease;
flex-shrink: 0;
gap: 2px;
}

/* Compact action buttons in selection mode */
#conversations-actions .btn {
padding: 2px 4px !important;
font-size: 0.7rem !important;
margin-right: 0 !important;
line-height: 1;
}

/* Reduce toggle row padding when selection actions are visible */
#conversations-toggle.selection-active {
padding-left: 0.5rem !important;
padding-right: 0.25rem !important;
}

#sidebar-delete-selected-btn {
Expand Down
Binary file removed application/single_app/static/images/custom_logo.png
Binary file not shown.
Binary file not shown.
Loading
Loading