Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 tools/ess-ca-to-da/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Deterministic line endings for this tool, independent of a contributor's
# core.autocrlf setting. Text is stored and checked out as LF; binaries are
# auto-detected and left untouched.
* text=auto eol=lf
6 changes: 6 additions & 0 deletions tools/ess-ca-to-da/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
out/
*.egg-info/
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
411 changes: 411 additions & 0 deletions tools/ess-ca-to-da/README.md

Large diffs are not rendered by default.

297 changes: 297 additions & 0 deletions tools/ess-ca-to-da/docs/DEV_DESIGN.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions tools/ess-ca-to-da/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[project]
name = "ess-ca-to-da"
version = "0.1.0"
description = "Migrate ESS Custom Engine Agent (Dataverse) customizations onto the ESS Declarative Agent (ALM package)."
requires-python = ">=3.11"
dependencies = [
"httpx>=0.28.1",
"msal>=1.37.0",
"ruamel.yaml>=0.18.0",
]

[project.scripts]
ess-ca-to-da = "essmig.cli:main"

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.6", "mypy>=1.11"]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.ruff]
line-length = 100
src = ["src", "tests"]

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

[tool.mypy]
python_version = "3.11"
strict = true
files = ["src"]

[[tool.mypy.overrides]]
module = "msal"
ignore_missing_imports = true
1,989 changes: 1,989 additions & 0 deletions tools/ess-ca-to-da/reference/ca-baseline.json

Large diffs are not rendered by default.

431 changes: 431 additions & 0 deletions tools/ess-ca-to-da/reference/core/agent.yml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions tools/ess-ca-to-da/reference/core/app.config.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"formatVersion": "1.0",
"realm": "dev",
"values": {
"botName": "Employee Self-Service Agent (Preview)",
"gptDisplayName": "Employee Self-Service Agent (Preview)"
},
"connections": {},
"flows": {},
"secrets": {}
}
7 changes: 7 additions & 0 deletions tools/ess-ca-to-da/reference/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"formatVersion": "1.0",
"packageType": "templated",
"publisher": "microsoftfirstparty",
"templateVersion": "1.0.0",
"createdUtc": "2026-08-11T00:00:00Z"
}
10 changes: 10 additions & 0 deletions tools/ess-ca-to-da/reference/core/reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"vertical": "core",
"vendored_utc": "2026-09-18T20:07:43+00:00",
"ess_root": "C:\\ESSVivaCopilot\\sources\\dev",
"da_agent_folder": "gptagent_copilotforemployeeselfservicecore",
"template_version": "1.0.0",
"package_type": "templated",
"publisher": "microsoftfirstparty",
"baseline_components": 7
}
12,085 changes: 12,085 additions & 0 deletions tools/ess-ca-to-da/reference/hr/agent.yml

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions tools/ess-ca-to-da/reference/hr/app.config.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"formatVersion": "1.0",
"realm": "dev",
"values": {
"botName": "Employee Self-Service HR (Preview)",
"gptDisplayName": "Employee Self-Service HR (Preview)"
},
"connections": {
"gptagent_copilotforemployeeselfservicehr.a1f4c28d-6b7c-49b9-a32e-55d8f19c7a03.shared_service-now": {
"connectorId": "/providers/Microsoft.PowerApps/apis/shared_service-now",
"connectionId": null
}
},
"flows": {
"3164dae9-3a2b-5843-98dd-e62bb5123324": "3164dae9-3a2b-5843-98dd-e62bb5123324",
"eab2ffdb-27cb-588b-a640-c2693acce57e": "eab2ffdb-27cb-588b-a640-c2693acce57e"
},
"secrets": {}
}
7 changes: 7 additions & 0 deletions tools/ess-ca-to-da/reference/hr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"formatVersion": "1.0",
"templateVersion": "1.0.1",
"packageType": "templated",
"publisher": "microsoftfirstparty",
"createdUtc": "2026-07-27T00:00:00Z"
}
10 changes: 10 additions & 0 deletions tools/ess-ca-to-da/reference/hr/reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"vertical": "hr",
"vendored_utc": "2026-09-18T20:07:43+00:00",
"ess_root": "C:\\ESSVivaCopilot\\sources\\dev",
"da_agent_folder": "gptagent_copilotforemployeeselfservicehr",
"template_version": "1.0.1",
"package_type": "templated",
"publisher": "microsoftfirstparty",
"baseline_components": 141
}
11,846 changes: 11,846 additions & 0 deletions tools/ess-ca-to-da/reference/it/agent.yml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions tools/ess-ca-to-da/reference/it/app.config.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"formatVersion": "1.0",
"realm": "dev",
"values": {
"botName": "Employee Self-Service IT (Preview)",
"gptDisplayName": "Employee Self-Service IT (Preview)"
},
"connections": {
"gptagent_copilotforemployeeselfserviceit.shared_alchemy.shared-alchemy-8262076a-e778-450b-8a35-5ae815712319": {
"connectorId": "/providers/Microsoft.PowerApps/apis/shared_alchemy",
"connectionId": null
},
"gptagent_copilotforemployeeselfserviceit.3f7a2b1c-8d6e-4a5b-9c0d-2e3f4a5b6c7d.shared_service-now": {
"connectorId": "/providers/Microsoft.PowerApps/apis/shared_service-now",
"connectionId": null
}
},
"flows": {
"3164dae9-3a2b-5843-98dd-e62bb5123324": "3164dae9-3a2b-5843-98dd-e62bb5123324",
"eab2ffdb-27cb-588b-a640-c2693acce57e": "eab2ffdb-27cb-588b-a640-c2693acce57e"
},
"secrets": {}
}
7 changes: 7 additions & 0 deletions tools/ess-ca-to-da/reference/it/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"formatVersion": "1.0",
"templateVersion": "1.0.1",
"packageType": "templated",
"publisher": "microsoftfirstparty",
"createdUtc": "2026-07-27T00:00:00Z"
}
10 changes: 10 additions & 0 deletions tools/ess-ca-to-da/reference/it/reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"vertical": "it",
"vendored_utc": "2026-09-18T20:07:43+00:00",
"ess_root": "C:\\ESSVivaCopilot\\sources\\dev",
"da_agent_folder": "gptagent_copilotforemployeeselfserviceit",
"template_version": "1.0.1",
"package_type": "templated",
"publisher": "microsoftfirstparty",
"baseline_components": 135
}
3 changes: 3 additions & 0 deletions tools/ess-ca-to-da/src/essmig/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""ESS Custom Engine Agent → Declarative Agent migration tool."""

__version__ = "0.1.0"
4 changes: 4 additions & 0 deletions tools/ess-ca-to-da/src/essmig/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from essmig.cli import main

if __name__ == "__main__":
raise SystemExit(main())
173 changes: 173 additions & 0 deletions tools/ess-ca-to-da/src/essmig/assessment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Is this customer ready to migrate, and if not, what is in the way?

The migration spec's leading success metric counts customers who have been
*assessed for scenario parity* and told either that they are eligible or what is
blocking them. That is a verdict, not a pile of counts, so the tool states one
plainly and separates the three things a Maker actually needs to tell apart:

* **Blockers** — capability that does not survive the move, at all. Nobody can
close these by working harder; they are a decision to accept a loss or wait.
* **Worklist** — real work, but work that can be done: conflicts to resolve and
topics to rebuild on supported primitives.
* **Employee impact** — what changes for the people using the agent every day,
which is invisible in a component-by-component report and is exactly what the
spec insists must not be discovered after cutover.

The verdict is deliberately conservative. ``ready`` means the tool found nothing
needing a human, not that the agent is proven correct — only the Maker's own
testing establishes that.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum

from essmig.merge import _AGENT_SUFFIX, ComponentResult, MergeResult, Outcome


class Eligibility(StrEnum):
READY = "ready"
"""Everything carried across; the Maker's job is to test and publish."""

NEEDS_WORK = "needs-work"
"""Migratable, but with conflicts to resolve or topics to rebuild first."""

BLOCKED = "blocked"
"""Something the customer relies on cannot be migrated at all."""


_VERDICT = {
Eligibility.READY: "Ready to migrate",
Eligibility.NEEDS_WORK: "Can migrate, with work to do first",
Eligibility.BLOCKED: "Blocked — some capability cannot be migrated",
}

_HISTORY_IMPACT = (
"Employee conversation history does not move. The Declarative Agent starts "
"with no history of conversations held with the Custom Engine Agent, so any "
"employee mid-way through a multi-turn task at cutover starts over."
)
_ANALYTICS_IMPACT = (
"Usage analytics start from zero. Reporting is per-agent, so the Declarative "
"Agent will not continue the Custom Engine Agent's adoption trend line. Export "
"the reports leaders depend on before cutover if you need the history."
)
_ENTRY_POINT_IMPACT = (
"This tool does not move employees between agents. It prepares the Declarative "
"Agent's content; how employees are cut over — and whether they must re-find or "
"re-pin the agent — is decided by how the agent is published and distributed."
)


@dataclass(frozen=True)
class Assessment:
eligibility: Eligibility
blockers: list[str] = field(default_factory=list)
worklist: list[str] = field(default_factory=list)
employee_impact: list[str] = field(default_factory=list)

@property
def verdict(self) -> str:
return _VERDICT[self.eligibility]

def to_json(self) -> dict[str, object]:
return {
"eligibility": self.eligibility.value,
"verdict": self.verdict,
"blockers": self.blockers,
"worklist": self.worklist,
"employeeImpact": self.employee_impact,
}


def assess(merged: MergeResult) -> Assessment:
"""The migration verdict for one agent."""
blockers = _blockers(merged.results)
worklist = _worklist(merged.results)
if blockers:
eligibility = Eligibility.BLOCKED
elif worklist:
eligibility = Eligibility.NEEDS_WORK
else:
eligibility = Eligibility.READY
return Assessment(
eligibility=eligibility,
blockers=blockers,
worklist=worklist,
employee_impact=_employee_impact(merged.results),
)


def _blockers(results: list[ComponentResult]) -> list[str]:
blockers: list[str] = []
for result in results:
name = _name(result)
if result.outcome is Outcome.FAILED:
blockers.append(f"{name} could not be processed: {result.detail}")
elif result.outcome is Outcome.BLOCKED:
blockers.append(f"{name} cannot be migrated. {result.detail}")
elif result.outcome is Outcome.NO_TARGET:
blockers.append(
f"{name} is a {result.component_type_label} customization this tool "
"does not know how to migrate. Its configuration is in the report — "
"check by hand whether the Declarative Agent supports it."
)
elif result.outcome is Outcome.LOCKED:
blockers.append(
f"{name} is locked by the ESS template, so your change to it cannot be kept."
)
elif result.needs_platform_support:
blockers.append(
f"{name} uses a capability the Declarative Agent does not support yet, "
"with no workaround available: " + "; ".join(result.unsupported)
)
return blockers


def _worklist(results: list[ComponentResult]) -> list[str]:
worklist: list[str] = []
for result in results:
name = _name(result)
if result.suffix == _AGENT_SUFFIX:
if result.outcome is Outcome.CONFLICTED:
worklist.append(f"{name}: {result.detail}")
continue
if result.outcome is Outcome.CONFLICTED:
count = len(result.conflicts)
worklist.append(
f"{name}: re-apply your change by hand — ESS changed the same "
f"{'place' if count == 1 else f'{count} places'} in this topic."
)
if result.outcome is Outcome.MANUAL:
worklist.append(
f"{name}: re-create this {result.component_type_label.lower()} in the "
"agent's settings after importing — the package cannot carry it. Your "
"configuration is reproduced in the report."
)
if result.deprecated and result.needs_maker_work:
worklist.append(
f"{name}: rebuild on supported building blocks — "
+ "; ".join(result.unsupported)
)
return worklist


def _employee_impact(results: list[ComponentResult]) -> list[str]:
impact = [_HISTORY_IMPACT, _ANALYTICS_IMPACT, _ENTRY_POINT_IMPACT]
disabled = [result for result in results if result.deprecated]
if disabled:
impact.insert(
0,
f"{len(disabled)} topic(s) are disabled in the package. Until you rebuild "
"them, employees who ask for those scenarios will fall through to a "
"general answer instead of the flow they get today.",
)
return impact


def _name(result: ComponentResult) -> str:
return result.display_name or result.suffix


__all__ = ["Assessment", "Eligibility", "assess"]
Loading
Loading