diff --git a/application/single_app/config.py b/application/single_app/config.py index e4efb1724..a21fa6be3 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.276" +VERSION = "0.261.277" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/v2_ui/src/components/workspace/WorkspaceEditorFrame.tsx b/application/v2_ui/src/components/workspace/WorkspaceEditorFrame.tsx index ea36f08ff..d8dece198 100644 --- a/application/v2_ui/src/components/workspace/WorkspaceEditorFrame.tsx +++ b/application/v2_ui/src/components/workspace/WorkspaceEditorFrame.tsx @@ -69,13 +69,15 @@ function SectionIcon({ icon: Icon = FileText }: { icon?: LucideIcon }) { * because an editor saves one record as a whole. */ export function WorkspaceEditorFrame({ - title, description, icon: TitleIcon, backTo, sections, dirty, saving, readOnly = false, error, + title, description, icon: TitleIcon, iconNode, backTo, sections, dirty, saving, readOnly = false, error, onSave, onDiscard, saveLabel = 'Save changes', actions, saveDisabled = false, initialSection, }: { title: string; description?: string; /** The kind of record being edited, shown beside the title. */ icon?: LucideIcon; + /** The record's own icon, such as an agent's uploaded image; shown in place of `icon` when given. */ + iconNode?: ReactNode; backTo: string; sections: WorkspaceEditorSection[]; dirty: boolean; @@ -250,10 +252,10 @@ export function WorkspaceEditorFrame({
- {TitleIcon ? ( + {iconNode || TitleIcon ? ( ) : null}
diff --git a/application/v2_ui/src/components/workspaceAgents/AgentActionPicker.tsx b/application/v2_ui/src/components/workspaceAgents/AgentActionPicker.tsx index 1679a68ac..81b3c2ce3 100644 --- a/application/v2_ui/src/components/workspaceAgents/AgentActionPicker.tsx +++ b/application/v2_ui/src/components/workspaceAgents/AgentActionPicker.tsx @@ -56,7 +56,7 @@ export function AgentActionPicker({
    {draft.actions_to_load.map((reference) => { const action = resolveAgentAction(reference, actions); - return
  • {action ? agentActionLabel(action) : reference}
  • ; + return
  • {action ? agentActionLabel(action) : 'Unavailable action'}
  • ; })}
{!readOnly ? confirmDetach ? ( @@ -127,9 +127,8 @@ export function AgentActionPicker({ : action.is_global ? Provided : Personal} {action.description} - ID: {action.id} {action.type === 'agent' && target ? ( - Calls {targetDetails?.display_name || targetDetails?.name || target.id} · {target.scope_type} + Calls {targetDetails?.display_name || targetDetails?.name || 'another agent'} · {target.scope_type} ) : null} {reason ? {reason}{checked ? ' This saved reference remains until you remove it.' : ''} : null} @@ -156,10 +155,10 @@ export function AgentActionPicker({ {unresolved.length ? ( - {unresolved.map((reference) => ( + {unresolved.map((reference, index) => (
- {reference} - {!readOnly ? Unavailable action {unresolved.length > 1 ? index + 1 : ''} + {!readOnly ? 1 ? ` ${index + 1}` : ''}`} onClick={() => setDraft((current) => ({ ...current, actions_to_load: current.actions_to_load.filter((item) => item !== reference) }))}> Remove reference : null} diff --git a/application/v2_ui/src/components/workspaceAgents/AgentIdentityFields.tsx b/application/v2_ui/src/components/workspaceAgents/AgentIdentityFields.tsx index e4defdd4a..c5634cdb5 100644 --- a/application/v2_ui/src/components/workspaceAgents/AgentIdentityFields.tsx +++ b/application/v2_ui/src/components/workspaceAgents/AgentIdentityFields.tsx @@ -165,9 +165,8 @@ export function AgentIdentityFields({ setDraft((current) => ({ ...current, name: value }))} /> -

Stable ID: {draft.id || 'Allocated when this agent is first saved.'}

); diff --git a/application/v2_ui/src/components/workspaceAgents/AgentKnowledgeFields.tsx b/application/v2_ui/src/components/workspaceAgents/AgentKnowledgeFields.tsx index 97b0b7373..cb98e83ec 100644 --- a/application/v2_ui/src/components/workspaceAgents/AgentKnowledgeFields.tsx +++ b/application/v2_ui/src/components/workspaceAgents/AgentKnowledgeFields.tsx @@ -14,6 +14,16 @@ import { EditorDependents, EditorFieldset, EditorGroup, EditorPanel, EditorSwitc import { SectionSearch } from '../workspace/primitives'; import { AgentField, AgentNotice, AgentTextField } from './AgentFields'; +const SOURCE_TYPE_LABELS: Record = { + personal: 'Personal workspace', + group: 'Group workspace', + public: 'Public workspace', +}; + +function sourceTypeLabel(scope: string): string { + return SOURCE_TYPE_LABELS[scope] ?? 'Workspace'; +} + export function AgentKnowledgeFields({ draft, setDraft, catalog, loading, error, onRefresh, readOnly, knowledgeScopes = ['personal', 'public'], }: { @@ -44,7 +54,7 @@ export function AgentKnowledgeFields({ const tags = catalog?.tags ?? []; const unavailableTags = config.tags.filter((tag) => !tags.some((item) => item.name === tag)); const visibleDocuments = documents.filter((document) => - `${document.title} ${document.file_name} ${document.source_name} ${document.id}`.toLowerCase().includes(documentQuery.toLowerCase()) && + `${document.title} ${document.file_name} ${document.source_name}`.toLowerCase().includes(documentQuery.toLowerCase()) && (!selectedKeys.size || selectedKeys.has(`${document.scope}:${document.source_id}`) || config.document_ids.includes(document.id))); const active = catalog ? resolvedAgentDocuments(config, catalog) : []; @@ -98,19 +108,19 @@ export function AgentKnowledgeFields({ : 'Only public workspaces can be assigned, because a global agent answers everyone.'} Removing a source does not silently remove stored document references.`}>
- {sources.filter((source) => `${source.label} ${source.id}`.toLowerCase().includes(sourceQuery.toLowerCase())).map((source) => ( + {sources.filter((source) => `${source.label} ${sourceTypeLabel(source.scope)}`.toLowerCase().includes(sourceQuery.toLowerCase())).map((source) => ( ))}
{!sources.length && !loading && !error ?

No authorized workspace sources are available. You may still assign URLs.

: null} {unavailableSourceKeys.map((key) => (
- Saved source unavailable here: {key} - {!readOnly ? { + Saved {sourceTypeLabel(key.split(':')[0]).toLowerCase()} is no longer available here + {!readOnly ? { setDraft((current) => { const currentConfig = readAgentKnowledge(current); return updateAgentKnowledge(current, { scopes: { @@ -147,7 +157,7 @@ export function AgentKnowledgeFields({ }); })} /> {document.title || document.file_name} - {document.source_name} · {document.id} + {[document.source_name, document.title && document.file_name !== document.title ? document.file_name : ''].filter(Boolean).join(' · ')} {document.tags.length ? {document.tags.join(', ')} : null} {checked && !inSources ? Saved reference retained; its workspace is not selected. : null} @@ -156,10 +166,10 @@ export function AgentKnowledgeFields({ })}
{visibleDocuments.length > documentLimit ? setDocumentLimit((value) => value + 25)}>Show more documents ({visibleDocuments.length - documentLimit} remaining) : null} - {unavailableDocuments.map((id) => ( + {unavailableDocuments.map((id, index) => (
- Unavailable document: {id} - {!readOnly ? Saved document {unavailableDocuments.length > 1 ? `${index + 1} ` : ''}is no longer available + {!readOnly ? 1 ? ` ${index + 1}` : ''}`} onClick={() => setDraft((current) => updateAgentKnowledge(current, { document_ids: readAgentKnowledge(current).document_ids.filter((item) => item !== id) }))}>Remove reference : null}
))} diff --git a/application/v2_ui/src/components/workspaceAgents/AgentModelFields.tsx b/application/v2_ui/src/components/workspaceAgents/AgentModelFields.tsx index dde7d1cdd..6836c031b 100644 --- a/application/v2_ui/src/components/workspaceAgents/AgentModelFields.tsx +++ b/application/v2_ui/src/components/workspaceAgents/AgentModelFields.tsx @@ -8,8 +8,9 @@ import type { WorkspaceModelEndpoint } from '../../lib/types'; import { AGENT_INPUT_CLASS, FOUNDRY_SETTINGS_KEYS, agentModelChoices, agentText, applyFoundryDiscovery, clearAgentDraftFields, foundryEndpointMatches, foundrySettings, selectAgentModel, selectedAgentModel, selectFoundryEndpoint, - updateAgentSetting, type FoundryDiscoveryRecord, + updateAgentSetting, type AgentModelChoice, type FoundryDiscoveryRecord, } from '../../lib/workspaceAgentAuthoring'; +import { PROVIDER_OPTIONS } from '../../lib/modelConnections'; import { normalizeAgentKnowledgeUrl } from '../../lib/workspaceAgentKnowledge'; import { GlassButton } from '../ui/primitives'; import { EditorGroup, EditorRow, EditorSwitch } from '../workspace/EditorLayout'; @@ -46,6 +47,35 @@ interface ModelFieldsProps { neutralReadOnlyCopy?: boolean; } +const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** A stored value only when it is a readable name; internal identifiers are never shown. */ +function readableName(value: unknown): string { + const text = agentText(value).trim(); + return text && !GUID_PATTERN.test(text) ? text : ''; +} + +function providerLabel(provider: string | undefined): string { + if (!provider) return ''; + return PROVIDER_OPTIONS.find((option) => option.value === provider)?.label ?? readableName(provider); +} + +/** What the agent calls, in the names people configured: deployment, model, connection and provider. */ +function currentSelection(draft: AgentConfiguration, selected: AgentModelChoice | undefined): Array<[string, string]> { + const storedDeployment = draft.enable_agent_gpt_apim ? draft.azure_agent_apim_gpt_deployment : draft.azure_openai_gpt_deployment; + const deployment = readableName(selected?.deployment) || readableName(storedDeployment) || readableName(draft.model_id); + const model = readableName(selected?.modelName) || deployment; + const connection = selected?.apim || draft.enable_agent_gpt_apim ? 'Azure API Management' + : readableName(selected?.endpointName) || (draft.model_endpoint_id ? 'Saved connection' : 'Configured default'); + const provider = providerLabel(selected?.provider || draft.model_provider) || 'Configured default'; + return [ + ['Deployment', deployment || 'Configured default'], + ['Model', model || 'Configured default'], + ['Connection', connection], + ['Provider', provider], + ]; +} + function LocalModelFields({ draft, setDraft, options, original, allowCustomEndpoints, neutralReadOnlyCopy }: ModelFieldsProps) { const choices = agentModelChoices(options); const selected = selectedAgentModel(draft, choices); @@ -62,7 +92,8 @@ function LocalModelFields({ draft, setDraft, options, original, allowCustomEndpo const choice = choices.find((item) => item.key === event.target.value); if (choice) setDraft((current) => selectAgentModel(current, choice)); }}> - + {choices.map((choice) => )} @@ -70,10 +101,10 @@ function LocalModelFields({ draft, setDraft, options, original, allowCustomEndpo ? 'Uses a configured model.' : 'No enabled models are listed. You can retain the saved connection or configure a custom connection below.'} : null} -
-
Endpoint ID
{draft.model_endpoint_id || 'Legacy / configured default'}
-
Model ID
{draft.model_id || draft.azure_openai_gpt_deployment || draft.azure_agent_apim_gpt_deployment || 'Configured default'}
-
Provider
{draft.model_provider || 'Configured default'}
+
+ {currentSelection(draft, selected).map(([term, value]) => ( +
{term}
{value}
+ ))}
@@ -203,8 +234,8 @@ function FoundryModelFields({ draft, setDraft, options, neutralReadOnlyCopy, dis }}> {endpointId && !selectedEndpoint ? : null} - {endpoints.map((endpoint) => )} + ? 'Uses a configured model' : 'Saved connection unavailable'} : null} + {endpoints.map((endpoint) => )}
diff --git a/application/v2_ui/src/lib/workspaceAgentAuthoring.ts b/application/v2_ui/src/lib/workspaceAgentAuthoring.ts index dee772137..29088dc28 100644 --- a/application/v2_ui/src/lib/workspaceAgentAuthoring.ts +++ b/application/v2_ui/src/lib/workspaceAgentAuthoring.ts @@ -210,6 +210,8 @@ export interface AgentModelChoice { key: string; id: string; endpointId: string; + /** The connection's display name, shown instead of its identifier. */ + endpointName: string; provider: string; deployment: string; modelName: string; @@ -232,10 +234,11 @@ export function agentModelChoices(options: AgentEditorOptions): AgentModelChoice key: JSON.stringify([endpoint.id, id]), id, endpointId: endpoint.id, + endpointName: agentText(endpoint.name), provider: agentText(endpoint.provider) || 'aoai', deployment, modelName: agentText(model.modelName || model.name || id), - label: `${agentText(model.displayName) || deployment} · ${endpoint.name || endpoint.id}`, + label: `${agentText(model.displayName) || deployment} · ${endpoint.name || 'Unnamed connection'}`, apim: false, }); } @@ -244,7 +247,7 @@ export function agentModelChoices(options: AgentEditorOptions): AgentModelChoice const settings = options.settings; if (settings.enable_gpt_apim === true) { return agentText(settings.azure_apim_gpt_deployment).split(',').map((item) => item.trim()).filter(Boolean).map((id) => ({ - key: JSON.stringify(['apim', id]), id, endpointId: '', provider: '', + key: JSON.stringify(['apim', id]), id, endpointId: '', endpointName: '', provider: '', deployment: id, modelName: id, label: id, apim: true, })); } @@ -254,7 +257,7 @@ export function agentModelChoices(options: AgentEditorOptions): AgentModelChoice const id = agentText(model.id || model.deploymentName || model.deployment || model.modelName || model.name); if (!id) continue; choices.push({ - key: JSON.stringify(['legacy', id]), id, endpointId: '', provider: '', + key: JSON.stringify(['legacy', id]), id, endpointId: '', endpointName: '', provider: '', deployment: agentText(model.deploymentName || model.deployment || id), modelName: agentText(model.modelName || model.name || id), label: agentText(model.display_name || model.displayName || model.deploymentName || model.deployment || id), diff --git a/application/v2_ui/src/pages/workspace/AgentEditorPage.tsx b/application/v2_ui/src/pages/workspace/AgentEditorPage.tsx index 77d26b593..0555d54ab 100644 --- a/application/v2_ui/src/pages/workspace/AgentEditorPage.tsx +++ b/application/v2_ui/src/pages/workspace/AgentEditorPage.tsx @@ -6,7 +6,7 @@ import { BookOpen, Cpu, IdCard, LayoutTemplate, Plug, ScrollText, SlidersHorizon import { GlassButton } from '../../components/ui/primitives'; import { WorkspaceEditorFrame } from '../../components/workspace/WorkspaceEditorFrame'; import { SectionSkeleton } from '../../components/workspace/primitives'; -import { AgentIdentityFields } from '../../components/workspaceAgents/AgentIdentityFields'; +import { AgentIcon, AgentIdentityFields } from '../../components/workspaceAgents/AgentIdentityFields'; import { AgentModelFields } from '../../components/workspaceAgents/AgentModelFields'; import { AgentActionPicker } from '../../components/workspaceAgents/AgentActionPicker'; import { AgentKnowledgeFields } from '../../components/workspaceAgents/AgentKnowledgeFields'; @@ -201,7 +201,8 @@ function AgentEditorSession({ resourceId, scope, adapter }: { resourceId: string return ( : undefined} + description={isNew ? 'Configure a reusable assistant. Changes are saved only when you choose Save agent.' : draft.description || undefined} backTo={adapter.basePath} dirty={dirty || iconBusy} saving={saving} readOnly={readOnly} error={arraySecretError || saveError} onSave={() => void save()} onDiscard={clear} saveLabel="Save agent" saveDisabled={Boolean(advancedError) || iconBusy} initialSection={new URLSearchParams(location.search).get('templates') === '1' ? 'templates' : undefined} diff --git a/docs/explanation/fixes/AGENT_EDITOR_INTERNAL_ID_EXPOSURE_FIX.md b/docs/explanation/fixes/AGENT_EDITOR_INTERNAL_ID_EXPOSURE_FIX.md new file mode 100644 index 000000000..7abf10643 --- /dev/null +++ b/docs/explanation/fixes/AGENT_EDITOR_INTERNAL_ID_EXPOSURE_FIX.md @@ -0,0 +1,40 @@ +# Agent Editor Internal ID Exposure Fix + +Fixed in version: **0.261.277** + +## Issue + +The shared agent editor, which is used for personal, group and global agents, showed internal identifiers that mean nothing to users: + +- The header showed "Stable ID: ", and a generic icon appeared instead of the agent's own logo (for example the Microsoft 365 PNG shown on the agents list). +- The Identity section repeated the stable ID. +- Model & connection → Current selection showed the Endpoint ID, the Model ID and raw provider codes such as `aoai`. +- The action picker showed `ID: ` on every action. +- In Assigned knowledge, source workspaces showed ` · ` and documents showed ` · `. Unavailable references printed the raw key. + +## Root cause + +The editor was built as an authoring surface and rendered the stored references directly, because they were the only values guaranteed to exist. + +## Changes + +- `WorkspaceEditorFrame.tsx`: added an optional `iconNode` prop for the title tile. +- `AgentEditorPage.tsx`: passes ``, the same component the agents list uses. The header subtitle is now the agent description instead of the stable ID. +- `AgentIdentityFields.tsx`: removed the stable ID line. +- `workspaceAgentAuthoring.ts`: `AgentModelChoice` now carries `endpointName`, and unnamed connections fall back to "Unnamed connection" instead of the endpoint ID. +- `AgentModelFields.tsx`: Current selection now shows **Deployment**, **Model**, **Connection** and **Provider**: + - The provider shows its friendly label, such as "Azure OpenAI". + - Values that are GUIDs are never shown; a readable fallback appears instead. +- `AgentActionPicker.tsx`: removed action IDs. Unresolved references show "Unavailable action [n]". +- `AgentKnowledgeFields.tsx`: + - Workspaces show their type ("Personal workspace", "Group workspace" or "Public workspace"). + - Documents show the workspace and file name, and keep their tags. + - Unavailable references use friendly text. + - IDs are no longer part of the search text. + +The stored data is unchanged. Only how it is presented changed. + +## Validation + +- `functional_tests/test_agent_editor_hides_internal_ids.py` +- `npm run typecheck` in `application/v2_ui` diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index a5c0a6925..ae18e9a8c 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,16 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.277)** + +#### User Interface Enhancements + +* **Agent Editor No Longer Shows GUIDs** + * The agent editor for personal, group and global agents no longer shows internal identifiers. The header shows the agent's own icon, such as the Microsoft 365 logo, and its description instead of a stable ID. + * Model & connection › Current selection lists the deployment, model, connection name and provider (for example "Azure OpenAI") instead of endpoint and model IDs. + * Action choices, knowledge workspaces and documents no longer show IDs. Workspaces show their type, and documents show their workspace, file name and tags. + * (Ref: `AgentEditorPage.tsx`, `AgentModelFields.tsx`, `AgentActionPicker.tsx`, `AgentKnowledgeFields.tsx`, [Agent Editor Internal ID Exposure Fix](fixes/AGENT_EDITOR_INTERNAL_ID_EXPOSURE_FIX.md)) + ### **(v0.261.276)** #### New Features diff --git a/functional_tests/test_agent_editor_hides_internal_ids.py b/functional_tests/test_agent_editor_hides_internal_ids.py new file mode 100644 index 000000000..d0fc60313 --- /dev/null +++ b/functional_tests/test_agent_editor_hides_internal_ids.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# test_agent_editor_hides_internal_ids.py +""" +Functional test for hiding internal identifiers in the shared agent editor. +Version: 0.261.277 +Implemented in: 0.261.277 + +This test ensures that the shared v2 agent editor (personal, group and global agents) +does not render GUIDs or internal IDs: no stable ID in the header or identity section, +no endpoint/model IDs in Current selection, no action IDs in the action picker, and no +workspace or document IDs in the knowledge fields. It also checks that the header shows +the agent's own icon and that documents show their file name. +""" + +import os +import sys + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from test_support.versioning import assert_app_version_at_least + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(REPO_ROOT, "application", "v2_ui", "src") + + +def read_source(*parts): + with open(os.path.join(SRC, *parts), encoding="utf-8") as handle: + return handle.read() + + +def test_header_shows_agent_icon_without_stable_id(): + """The editor header renders the agent icon and never the stable ID.""" + page = read_source("pages", "workspace", "AgentEditorPage.tsx") + frame = read_source("components", "workspace", "WorkspaceEditorFrame.tsx") + identity = read_source("components", "workspaceAgents", "AgentIdentityFields.tsx") + assert "Model ID<", "'Model ID'"): + assert removed not in model, removed + for label in ("'Deployment'", "'Model'", "'Connection'", "'Provider'"): + assert label in model, label + assert "PROVIDER_OPTIONS" in model + assert "GUID_PATTERN" in model + return True + + +def test_action_picker_hides_action_ids(): + """Action choices and unavailable references never render IDs.""" + picker = read_source("components", "workspaceAgents", "AgentActionPicker.tsx") + assert "ID: {action.id}" not in picker + assert "Unavailable action" in picker + return True + + +def test_knowledge_fields_hide_ids_and_show_file_names(): + """Workspaces show a friendly type, documents show file names, and no IDs render.""" + knowledge = read_source("components", "workspaceAgents", "AgentKnowledgeFields.tsx") + assert "{source.scope} · {source.id}" not in knowledge + assert "{document.source_name} · {document.id}" not in knowledge + assert "Unavailable document: {id}" not in knowledge + assert "Saved source unavailable here: {key}" not in knowledge + assert "${document.id}`.toLowerCase()" not in knowledge + assert "sourceTypeLabel" in knowledge + assert "document.file_name" in knowledge + return True + + +def test_version_floor(): + assert_app_version_at_least("0.261.277") + return True + + +if __name__ == "__main__": + tests = [ + test_header_shows_agent_icon_without_stable_id, + test_current_selection_uses_readable_names, + test_action_picker_hides_action_ids, + test_knowledge_fields_hide_ids_and_show_file_names, + test_version_floor, + ] + results = [] + for test in tests: + print(f"Running {test.__name__}...") + try: + results.append(test()) + except AssertionError as error: + print(f"Failed: {test.__name__}: {error}") + results.append(False) + print(f"Results: {sum(bool(r) for r in results)}/{len(results)} tests passed") + sys.exit(0 if all(results) else 1)