diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd3cdcc5..15b40ba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 2848704f..0fc8fe83 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -44,7 +44,7 @@ jobs: security-events: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.gitignore b/.gitignore index d159295a..648db26a 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,5 @@ FodyWeavers.xsd *.sln.iml .idea/ -.DS_Store \ No newline at end of file +.DS_Store +**/.venv/ diff --git a/QUICKSTART.md b/QUICKSTART.md index 592db408..0b41e188 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,6 +1,24 @@ -# Quick Start Guide for Dragon Extension Developer - -## 🚀 Getting Started (Choose One) +# Table of Contents +- [Quick Start Guide for Dragon Extension Developer](#quick-start-guide-for-dragon-extension-developer) +- [Running Locally](#-running-locally) + - Provides prerequisites and information on how to run the application locally. +- [Using DevTunnels](#using-devtunnels) + - How to create a secure way to expose your local web service to the internet without actually deploying. +- [Packaging your extension](#packaging-your-extension) + - How to package your extension using the dragon extension tool to to the DAC (Dragon Admin Center) site. +- [Create an Application in Azure portal that represents your application](#create-an-application-in-azure-portal-that-represents-your-application) + - How to register your extension in the Azure portal so it can be used with Dragon Copilot. +- [Installing your Extension](#installing-your-extension) + - How to install your extension you created onto the DAC site. +- [Testing your Extension](#testing-your-extension) + - How to test your extension in Dragon Copilot. + +# Quick Start Guide for a Dragon Extension Developer +This document is a quick‑start guide for building, testing, packaging, and deploying a custom Dragon Copilot extension. Its purpose is to walk an extension developer through the full development lifecycle—from setting up the environment to validating the extension inside the Dragon Copilot application. + +Prior to running through this document, you may want to read through the Microsoft Learn [documentation](https://learn.microsoft.com/en-us/industry/healthcare/dragon-copilot/extensions/workflow-app-overview) outlining how your extension will interface with the overall Dragon Copilot solution. + +## 🚀 Running Locally ### Development Prerequisites * DotNet 9 @@ -14,6 +32,7 @@ The application will start and be available at http://localhost:5181 +Some additional development concepts are located in the following Microsoft Learn [documentation](https://learn.microsoft.com/en-us/industry/healthcare/dragon-copilot/extensions/workflow-app-concepts). ### Call the endpoint You can make use of the [SampleExtension.Web.http](./samples/DragonCopilot/Workflow/SampleExtension.Web/SampleExtension.Web.http) file in the sample project to make a call. It contains a sample invocation for an extension listening for `Note` content. @@ -45,132 +64,171 @@ Transfer-Encoding: chunked } } ``` +Details about an adaptive card structure and what fields are valid is located in the [Adaptive Card Specification](https://learn.microsoft.com/en-us/industry/healthcare/dragon-copilot/extensions/adaptive-card-spec). -## ☁️ Deploy to Azure (Production Ready) - -### Prerequisites for Azure Deployment -- Docker Desktop installed and running -- Azure subscription with Container Apps deployed -- Container registry with permissions granted to the Container Apps identity - -### Steps - -#### 1. Build the Docker Image -From the **repository root directory**, build the Docker image: - -```powershell -# Build the Docker image -docker build -f samples/DragonCopilot/Workflow/SampleExtension.Web/Dockerfile -t dragon-extension:latest . -``` - -> **Note**: The Dockerfile must be built from the repository root because it references files from both `src/Dragon.Copilot.Models/` and `samples/DragonCopilot/Workflow/SampleExtension.Web/`. - -#### 2. Test the Docker Image Locally (Optional but Recommended) -```powershell -# Run the container locally -docker run -p 5181:8080 dragon-extension:latest - -# Test the health endpoint -curl http://localhost:5181/health -``` - -#### 3. Tag and Push to Azure Container Registry -```powershell -# Login to Azure -az login - -# Login to your Azure Container Registry -az acr login --name - -# Tag the image for your registry -docker tag dragon-extension:latest .azurecr.io/dragon-extension:latest - -# Push the image -docker push .azurecr.io/dragon-extension:latest -``` - -#### 4. Deploy to Azure Container Apps -```powershell -# Create or update the container app -az containerapp update ` - --name ` - --resource-group ` - --image .azurecr.io/dragon-extension:latest - -# Verify deployment -az containerapp show ` - --name ` - --resource-group ` - --query "properties.latestRevisionFqdn" ` - --output tsv -``` - -#### 5. Configure Environment Variables (Production) -For production deployments, configure authentication and other settings: - -```powershell -az containerapp update ` - --name ` - --resource-group ` - --set-env-vars ` - ASPNETCORE_ENVIRONMENT=Production ` - Authentication__Enabled=true ` - Authentication__TenantId= ` - Authentication__ClientId= ` - Authentication__Instance=https://login.microsoftonline.com/ -``` - -See [Authentication.md](./doc/Authentication.md) for detailed authentication configuration. - -#### 6. Verify Production Deployment -```powershell -# Get the FQDN -$fqdn = az containerapp show ` - --name ` - --resource-group ` - --query "properties.latestRevisionFqdn" ` - --output tsv - -# Test the health endpoint -curl "https://$fqdn/health" - -# View logs -az containerapp logs show ` - --name ` - --resource-group ` - --follow -``` - -## 📋 What the Service Does - -### Sample Extension (Port 5181) -- Example implementation of a Dragon Copilot extension -- Shows proper request/response handling for Dragon Copilot integration -- Includes health check endpoints -- Demonstrates error handling patterns -- Provides comprehensive API documentation via Swagger - -## 🔍 Troubleshooting - -### Services Won't Start -- Check .NET 9.0 SDK is installed: `dotnet --version` -- Make sure that you have nuget available as default source: `dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org` -- Ensure port 5181 is free - -### Integration Tests Fail -- Verify the extension is running and healthy -- Check extension logs in terminal window -- Test extension directly using the HTTP test file - -## 📚 Next Steps - -1. **Explore the APIs**: Use Swagger UI to understand the interfaces -3. **Create Your Extension**: Use `samples/DragonCopilot/Workflow/SampleExtension.Web` as a starting point -4. **Customize Business Logic**: Modify `ProcessingService.cs` for your needs -5. **Add Your Tests**: Extend the http test suite for your scenarios - -## 🎉 You're Ready! - -Your Dragon Extension Developer environment is now set up and ready for development. Start building your custom extensions and test them locally before deploying to Dragon Copilot. +### Making Code Changes +The majority of the code changes for your extension should fall underneath the [Process API](./samples/DragonCopilot/Workflow/SampleExtension.Web/Controllers/ProcessController.cs#L58-L95) method. The Process API will be called by Dragon Copilot to execute your extension. -For detailed documentation, see the individual README files in each project folder. +## Using DevTunnels +DevTunnels provide a secure way to expose your local web service to the internet without actually deploying. +1. [Install dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows#install). +2. In a terminal, issue a `devtunnel login` command and select your appropriate account. +3. Issue a `devtunnel create name-of-tunnel -a` command +4. Issue a `devtunnel port create name-of-tunnel -p 5181` command +5. Issue a `devtunnel host name-of-tunnel` command +6. Copy the URL that is output in the terminal. Make sure to copy the dev tunnel "connect via browser url" as shown in example below. + + ![devtunnel-url.png](doc/devtunnel-url.png) + +## Packaging your extension +We are now going to package our extension using the dragon-extension CLI tool. + +1. Open a new terminal window +2. Traverse to tools/dragon-extension-cli +3. Issue a `npm run build` command +4. Issue a `npm link` command +5. Issue a `dragon-extension init` command + You will be asked for information on your extension. + - Ensure the tenantId specified is for where you will upload your extension to. + - Ensure the api endpoint points to the process method using the devtunnel address you generated earlier. +6. Issue a `dragon-extension package` command +You now have a valid zip file that represents your extension! + +## Add the Service Principal to your tenant +> NOTE: We need to register the "Microsoft.HealthPlatform" resource provider in an Azure subscription that belongs to your tenant. This will inject a Service Principal (a.k.a. "Enterprise Application") for the Dragon Copilot Extension Runtime application registration into the Extension vendor's tenant. This step will only need to be done once for your tenant. + +1. Log into http://entra.microsoft.com +2. Go to Subscriptions and select your subscription. + ![](doc/subscriptions.png) + +5. Select Resource Providers on the left hand menu. + + ![](doc/resource-providers.png) + +6. Search for "Microsoft.HealthPlatform". + + ![](doc/health-platform-search.png) + +7. Select the entry and click the Register button. + + ![](doc/health-platform-register.png) + +## Create an Application in Azure portal that represents your application. +1. Log into http://entra.microsoft.com +2. Go to App registrations on the left menu + + ![](doc/entra-app-reg-menu.png) +3. Create a new registration + + ![](doc/entra-new-registration.png) +4. Name your application what you want and ensure it is a "Single tenant Application" + + ![](doc/entra-new-registration-name.png) +5. Once complete go to the "Expose an API" on the left side. + + ![](doc/entra-expose-an-api.png) +6. Add an Application ID URI. The format should be: `api://{entra-tenantid}/{devtunnelpath}` + - (i.e. api://1abcdefg3-n2g4-56dd-jj10-i34lmn5p7rst/k2dkm8r-7156.use.devtunnels.ms) + +5. Click the "Save" button +6. In the application details navigation, select "Token Configuration" + + ![](doc/entra-token-configuration.png) +7. Select "Add Optional Claim" in the details section + + ![](doc/entra-optional-claim.png) +8. For token type select "Access" and in the list of claims select "idtyp" + + ![](doc/entra-optional-claim-details.png) +9. Click the "Add" button +10. In the application details navigation, select "Manifest" + + ![](doc/entra-manifest.png) +11. Find the property "requestedAccessTokenVersion" and change the value from `null` to `2` + + ![](doc/entra-manifest-details.png) +12. Click the "Save" button + +## Installing your Extension + +1. Open the browser and go to `https://admin.healthplatform.microsoft.com/extensions` +2. Click the dropdown at the top to select the environment on the card you were given. + + ![](doc/switch-environment-menu.png) +3. In the page navigation click "Upload custom" + + ![](doc/dac-upload-custom.png) +4. Select the previously created zip file in the folder `tools/dragon` + +5. Agree to the terms + + ![](doc/dac-upload-custom-details.png) +6. Click the "Upload custom" button + +## Testing your Extension +1. Open the browser and go to `https://www.copilot.us.dragon.com` +2. Click "Sign In" +3. Allow the use of the microphone in the popup in the top left. +4. Go through the initial setup + 1. Select any Primary specialty + 2. Click "Next" + 3. Select any role + 4. Click "Next" + 5. Click "Complete setup" +5. Switch environment using following steps: + 1. Click "Settings" + ![settings-switch-environment-1.png](doc/settings-switch-environment-1.png) + 2. Click "General" + 3. Select your assigned environment from the dropdown + ![settings-switch-environment-2.png](doc/settings-switch-environment-2.png) + 4. Click "Reload app" in Change Environment popup + ![settings-switch-environment-3.png](doc/settings-switch-environment-3.png) + 5. Go through step 4 i.e. initial setup of selecting specialties. + +6. Ensure "Auto-style" is enabled + 1. Click the gear icon in the top right + + ![settings-gear.png](doc/settings-gear.png) + 3. Click "Note style & format" in the menu + + ![settings-note-style.png](doc/settings-note-style.png) + 5. Click "Style" in the menu + + ![settings-style.png](doc/settings-style.png) + 7. Toggle "Auto-style" to enabled + + ![settings-auto-style.png](doc/settings-auto-style.png) + 9. This setting is auto-saved and only needs to happen once +6. Ensure other extensions disabled + 1. Click the gear icon in the top right + + ![settings-gear.png](doc/settings-gear.png) + 2. Click "Extensions" in the menu + 3. Select extensions besides your own + 4. Toggle the extension off + 5. This setting is auto-saved and only needs to happen once +7. Click "Create patient session" in the bottom left + ![create-patient-session.png](doc/create-patient-session.png) +8. Create an ambient recording by clicking the button to the right of the prompt box in the bottom. + ![ambient-button.png](doc/ambient-button.png) + + You can refer to [audio recordings](./samples/audio-recordings) contained in the repository for examples of typical recordings. +10. Sample script: + + > Mr. John Doe is a 55-year-old male here for follow-up on hypertension. He's taking lisinopril 20 milligrams daily with good adherence. Blood pressure today is 128 over 78, heart rate 72. He reports no chest pain, shortness of breath, or headaches. He does note occasional mild dizziness when standing quickly, otherwise feels well. Exam is unremarkable, lungs are clear, heart regular, no edema. + > + > Assessment: Hypertension, well controlled. Mild orthostatic dizziness likely related to medication but not impacting daily function. + > + > Plan: Continue current lisinopril dose. Encourage hydration and slower positional changes. Reinforced diet and exercise recommendations. Ordered labs for next visit. Follow up in six months or sooner if symptoms worsen. + +7. Click the same button to stop recording. + +8. After recording is complete you should see a number of things happen: + * Recording uploaded + * Note generated + * Auto-style executed + * Extension executed and displayed in the Note section. + ![timeline-output.png](doc/timeline-output.png) + +9. Click the "Note" tab and scroll to the bottom to see your results + ![tab-note.png](doc/tab-note.png) diff --git a/README.md b/README.md index 7c6ccb2d..3f9c36c6 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,16 @@ Welcome! This repository contains sample code illustrating the Dragon Copilot ex ## 📚 Contents -- [Overview](#-overview) -- [Quick Start](#-quick-start) -- [Samples](#-samples) -- [Tools](#️-tools) -- [Contributing](#-contributing) -- [License](#-license) +- [Dragon Copilot Extension Samples](#dragon-copilot-extension-samples) + - [📚 Contents](#-contents) + - [📝 Overview](#-overview) + - [🚀 Quick Start](#-quick-start) + - [1. Clone \& Start (Windows)](#1-clone--start-windows) + - [📦 Samples](#-samples) + - [🛠️ Tools](#️-tools) + - [Dragon Extension CLI](#dragon-extension-cli) + - [🤝 Contributing](#-contributing) + - [📄 License](#-license) ## 📝 Overview @@ -21,7 +25,7 @@ This repo includes: - [Additional Documentation](doc/) ## 🚀 Quick Start - +Look here for a guide describing the process from downloading code to testing it in Dragon Copilot: [QUICKSTART.md](/QUICKSTART.md) ### 1. Clone & Start (Windows) ```powershell @@ -34,6 +38,7 @@ cd dragon-copilot-extension-samples | Sample Name | Description | Location | |--------------|------------- |----------| | Workflow Extension | C# Asp.Net WebApplication showing Dragon workflow extension API Contract | [SampleExtension.Web](./samples/DragonCopilot/Workflow/SampleExtension.Web/) | +| Audio Samples | Synthetic audio recordings of clinical encounters | [Audio-Recordings](./samples/audio-recordings/) | ## 🛠️ Tools diff --git a/doc/ambient-button.png b/doc/ambient-button.png new file mode 100644 index 00000000..ec654a5a Binary files /dev/null and b/doc/ambient-button.png differ diff --git a/doc/create-patient-session.png b/doc/create-patient-session.png new file mode 100644 index 00000000..a99e4151 Binary files /dev/null and b/doc/create-patient-session.png differ diff --git a/doc/dac-upload-custom-details.png b/doc/dac-upload-custom-details.png new file mode 100644 index 00000000..bb673f2c Binary files /dev/null and b/doc/dac-upload-custom-details.png differ diff --git a/doc/dac-upload-custom.png b/doc/dac-upload-custom.png new file mode 100644 index 00000000..a2fcb03b Binary files /dev/null and b/doc/dac-upload-custom.png differ diff --git a/doc/devtunnel-url.png b/doc/devtunnel-url.png new file mode 100644 index 00000000..9df588cb Binary files /dev/null and b/doc/devtunnel-url.png differ diff --git a/doc/entra-app-reg-menu.png b/doc/entra-app-reg-menu.png new file mode 100644 index 00000000..b3fff2ac Binary files /dev/null and b/doc/entra-app-reg-menu.png differ diff --git a/doc/entra-expose-an-api.png b/doc/entra-expose-an-api.png new file mode 100644 index 00000000..02ace1fc Binary files /dev/null and b/doc/entra-expose-an-api.png differ diff --git a/doc/entra-manifest-details.png b/doc/entra-manifest-details.png new file mode 100644 index 00000000..07470c06 Binary files /dev/null and b/doc/entra-manifest-details.png differ diff --git a/doc/entra-manifest.png b/doc/entra-manifest.png new file mode 100644 index 00000000..3ed0ccb2 Binary files /dev/null and b/doc/entra-manifest.png differ diff --git a/doc/entra-new-registration-name.png b/doc/entra-new-registration-name.png new file mode 100644 index 00000000..89854f3a Binary files /dev/null and b/doc/entra-new-registration-name.png differ diff --git a/doc/entra-new-registration.png b/doc/entra-new-registration.png new file mode 100644 index 00000000..3b6f0a5d Binary files /dev/null and b/doc/entra-new-registration.png differ diff --git a/doc/entra-optional-claim-details.png b/doc/entra-optional-claim-details.png new file mode 100644 index 00000000..e51ad557 Binary files /dev/null and b/doc/entra-optional-claim-details.png differ diff --git a/doc/entra-optional-claim.png b/doc/entra-optional-claim.png new file mode 100644 index 00000000..f634087b Binary files /dev/null and b/doc/entra-optional-claim.png differ diff --git a/doc/entra-token-configuration.png b/doc/entra-token-configuration.png new file mode 100644 index 00000000..36864f2d Binary files /dev/null and b/doc/entra-token-configuration.png differ diff --git a/doc/health-platform-register.png b/doc/health-platform-register.png new file mode 100644 index 00000000..dd005f2d Binary files /dev/null and b/doc/health-platform-register.png differ diff --git a/doc/health-platform-search.png b/doc/health-platform-search.png new file mode 100644 index 00000000..087c0dd8 Binary files /dev/null and b/doc/health-platform-search.png differ diff --git a/doc/resource-providers.png b/doc/resource-providers.png new file mode 100644 index 00000000..a9d06dc8 Binary files /dev/null and b/doc/resource-providers.png differ diff --git a/doc/settings-auto-style.png b/doc/settings-auto-style.png new file mode 100644 index 00000000..54db25e2 Binary files /dev/null and b/doc/settings-auto-style.png differ diff --git a/doc/settings-gear.png b/doc/settings-gear.png new file mode 100644 index 00000000..fc4d503b Binary files /dev/null and b/doc/settings-gear.png differ diff --git a/doc/settings-note-style.png b/doc/settings-note-style.png new file mode 100644 index 00000000..1cd5f961 Binary files /dev/null and b/doc/settings-note-style.png differ diff --git a/doc/settings-style.png b/doc/settings-style.png new file mode 100644 index 00000000..1ead7f9f Binary files /dev/null and b/doc/settings-style.png differ diff --git a/doc/settings-switch-environment-1.png b/doc/settings-switch-environment-1.png new file mode 100644 index 00000000..42bddd02 Binary files /dev/null and b/doc/settings-switch-environment-1.png differ diff --git a/doc/settings-switch-environment-2.png b/doc/settings-switch-environment-2.png new file mode 100644 index 00000000..3ba88831 Binary files /dev/null and b/doc/settings-switch-environment-2.png differ diff --git a/doc/settings-switch-environment-3.png b/doc/settings-switch-environment-3.png new file mode 100644 index 00000000..bbfd2fe0 Binary files /dev/null and b/doc/settings-switch-environment-3.png differ diff --git a/doc/subscriptions.png b/doc/subscriptions.png new file mode 100644 index 00000000..63dcc66d Binary files /dev/null and b/doc/subscriptions.png differ diff --git a/doc/switch-environment-menu.png b/doc/switch-environment-menu.png new file mode 100644 index 00000000..2dd84f56 Binary files /dev/null and b/doc/switch-environment-menu.png differ diff --git a/doc/tab-note.png b/doc/tab-note.png new file mode 100644 index 00000000..5f8b0fb2 Binary files /dev/null and b/doc/tab-note.png differ diff --git a/doc/timeline-output.png b/doc/timeline-output.png new file mode 100644 index 00000000..a2e9fda2 Binary files /dev/null and b/doc/timeline-output.png differ diff --git a/samples/DragonCopilot/Workflow/SampleExtension.Web/SampleExtension.Web.csproj b/samples/DragonCopilot/Workflow/SampleExtension.Web/SampleExtension.Web.csproj index e94b9530..e1b8fc48 100644 --- a/samples/DragonCopilot/Workflow/SampleExtension.Web/SampleExtension.Web.csproj +++ b/samples/DragonCopilot/Workflow/SampleExtension.Web/SampleExtension.Web.csproj @@ -7,10 +7,10 @@ - + - - + + diff --git a/samples/DragonCopilot/Workflow/SampleExtension.Web/Services/ProcessingService.cs b/samples/DragonCopilot/Workflow/SampleExtension.Web/Services/ProcessingService.cs index b06c483a..884b420c 100644 --- a/samples/DragonCopilot/Workflow/SampleExtension.Web/Services/ProcessingService.cs +++ b/samples/DragonCopilot/Workflow/SampleExtension.Web/Services/ProcessingService.cs @@ -200,17 +200,14 @@ private static VisualizationResource CreateAdaptiveCardResource(List { type = "TextBlock", text = "🔍 Clinical Entities Extracted", - weight = "Bolder", - size = "Large", - color = "Accent" + weight = "bolder", }, new { type = "TextBlock", text = $"Found {entities.Count} clinical {(entities.Count == 1 ? "entity" : "entities")} in the note", wrap = true, - size = "Medium", - spacing = "Small" + spacing = "small" } }; @@ -227,7 +224,7 @@ private static VisualizationResource CreateAdaptiveCardResource(List { type = "Container", style = "emphasis", - spacing = "Medium", + spacing = "medium", items = new object[] { new @@ -245,8 +242,7 @@ private static VisualizationResource CreateAdaptiveCardResource(List { type = "TextBlock", text = GetEntityIcon(entityType), - size = "Large", - spacing = "None" + spacing = "none" } } }, @@ -260,25 +256,21 @@ private static VisualizationResource CreateAdaptiveCardResource(List { type = "TextBlock", text = $"**{GetEntityTypeDisplayName(entityType)}**", - weight = "Bolder", - size = "Medium", - spacing = "None" + weight = "bolder", + spacing = "none" }, new { type = "TextBlock", text = entityName, - color = "Accent", - spacing = "None" + spacing = "none" }, new { type = "TextBlock", text = entityValue, wrap = true, - size = "Small", - color = "Default", - spacing = "Small" + spacing = "small" } } } @@ -314,22 +306,22 @@ private static VisualizationResource CreateAdaptiveCardResource(List { type = "TextBlock", text = $"Processed at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC", - size = "Small", horizontalAlignment = "Right", - spacing = "Medium" + spacing = "medium" }); return new VisualizationResource { Id = Guid.NewGuid().ToString(), Type = "AdaptiveCard", - Subtype = VisualizationSubtype.Note, + Subtype = VisualizationSubtype.Timeline, CardTitle = "Clinical Entities Extracted", - AdaptiveCardPayload = new + PartnerLogo = "https://example.com/assets/sample-extension-logo.png", + AdaptiveCardPayload = new AdaptiveCardPayload { - type = "AdaptiveCard", - version = "1.3", - body = bodyElements.ToArray() + Type = "AdaptiveCard", + Version = "1.3", + Body = bodyElements.ToArray(), }, Actions = new List { @@ -337,13 +329,14 @@ private static VisualizationResource CreateAdaptiveCardResource(List { Title = "Accept Analysis", Action = VisualizationActionType.Accept, - ActionType = ActionButtonType.Primary + ActionType = ActionButtonType.Accept, + Code = "Accept", }, new() { Title = "Copy to Note", Action = VisualizationActionType.Copy, - ActionType = ActionButtonType.Secondary, + ActionType = ActionButtonType.Copy, Code = "CLINICAL ENTITY ANALYSIS\n\nEntities detected:\n" + string.Join("\n", entities.Select(e => $"- {GetEntityNameFromResource(e)} ({GetEntityTypeFromResource(e)})")) }, @@ -351,9 +344,11 @@ private static VisualizationResource CreateAdaptiveCardResource(List { Title = "Reject Analysis", Action = VisualizationActionType.Reject, - ActionType = ActionButtonType.Tertiary + ActionType = ActionButtonType.Reject, + Code = "Reject" } }, + References = new List(), PayloadSources = new List { new() diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/Agents.md b/samples/DragonCopilot/Workflow/pythonSampleExtension/Agents.md deleted file mode 100644 index 691a22a9..00000000 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/Agents.md +++ /dev/null @@ -1,82 +0,0 @@ -## Agents Guide (Concise) - -Minimal contract for tools/agents working with the Python sample extension. - -### 1. Discovery Order -1. `$VIRTUAL_ENV/bin/python` -2. `.vscode/settings.json` → `python.defaultInterpreterPath` -3. `python3` in PATH -4. `python` in PATH -5. (Serve fallback) try `uvicorn` -Fail fast if none found. - -### 2. Use Existing Scripts -Prefer: -```bash -./scripts/start-python-dev.sh # run -./scripts/start-python-dev.sh --tests # tests -PORT=5182 ./scripts/start-python-dev.sh # alt port -``` -Avoid re-implementing interpreter logic. - -### 3. Test & Compare Flow -```bash -(PORT=5182 ./scripts/start-python-dev.sh &) # python service -./scripts/start-csharp-dev.sh # c# service -python3 scripts/compare_extensions.py --payload samples/requests/note-payload.json -``` -Parity when diff `notes` is empty. - -### 4. Dependency Sync (Optional) -Hash `requirements.txt`. If changed: -```bash -/path/python -m pip install -r samples/DragonCopilot/Workflow/pythonSampleExtension/requirements.txt --no-cache-dir -``` -Don’t uninstall arbitrary packages. - -### 5. Ports -Default 5181. If busy → choose 5182+. Log chosen port. - -### 6. Error Matrix -| Issue | Action | -|-------|--------| -| No interpreter | Stop; list attempted paths | -| Install fail | Retry once then abort | -| Port conflict | Increment up to +5 | -| Comparison diff | Report keys/types; no auto-fix | -| Missing tests dir | Recompute relative path | - -### 7. Minimal Checklist -- [ ] Interpreter resolved -- [ ] Dependencies current -- [ ] Tests pass -- [ ] Service (if needed) healthy -- [ ] Comparison (if dual) done -- [ ] Diffs (if any) reported - -### 8. Snippets -Interpreter candidates: -```bash -[[ -n "$VIRTUAL_ENV" ]] && echo "$VIRTUAL_ENV/bin/python"; \ -jq -r '."python.defaultInterpreterPath" // empty' .vscode/settings.json 2>/dev/null; \ -command -v python3; command -v python -``` -Module check: -```bash -python - <<'PY' -import importlib -for m in ("fastapi","uvicorn","pydantic"): - try: importlib.import_module(m); print("OK", m) - except Exception as e: print("MISSING", m, e) -PY -``` - -### 9. Safety -- Don’t edit `.vscode/settings.json` silently -- No PHI in test payloads -- Log only high-level paths & chosen interpreter - -### 10. Future (Optional) -`/internal/env`, lint/format flags, JSON logs, comparison task. - -End. diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/CreateVENV.md b/samples/DragonCopilot/Workflow/pythonSampleExtension/CreateVENV.md deleted file mode 100644 index 3d78d75c..00000000 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/CreateVENV.md +++ /dev/null @@ -1,36 +0,0 @@ -## (Optional) Create venv -```shell -pwd; -pushd $HOME/Code/VCS/ai/llm-train; - -VERSION="3.12"; -ENV_NAME="dgext"; -ENV_SUFFIX="pip"; -ENV_FULL_NAME="${ENV_NAME}${VERSION}${ENV_SUFFIX}"; -ENV_DIR="$HOME/Code/VENV"; -source ./envtools/create_env.sh -p "${ENV_DIR}/${ENV_FULL_NAME}" -v $VERSION; - -popd; -pwd; -``` - -## Activate venv -```shell -VERSION="3.12"; -ENV_NAME="dgext"; -ENV_SUFFIX="pip"; - -ENV_FULL_NAME="${ENV_NAME}${VERSION}${ENV_SUFFIX}"; - -ENV_DIR="$HOME/Code/VENV"; -PROJ_DIR="$HOME/Code/VCS/dragon/dragon-copilot-extension-samples"; - -SUB_PROJ="samples/DragonCopilot/Workflow/pythonSampleExtension"; -PACKAGE_FILE="${PROJ_DIR}/${SUB_PROJ}/requirements.txt"; - -source ${ENV_DIR}/${ENV_FULL_NAME}/bin/activate; -which python3; - -python3 -m pip install --upgrade pip; -python3 -m pip install -r ${PACKAGE_FILE} --no-cache; -``` \ No newline at end of file diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/README.md b/samples/DragonCopilot/Workflow/pythonSampleExtension/README.md index 46c53417..9f973e7a 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/README.md +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/README.md @@ -10,6 +10,22 @@ A Python FastAPI implementation that mirrors the C# `SampleExtension.Web` for Dr > Disclaimer: This is a learning/sample artifact – not production hardened. Do **not** use with real PHI. Authentication is intentionally disabled for development. +--- +## 📚 Contents + +- [Dragon Copilot Python Sample Extension](#dragon-copilot-python-sample-extension) + - [1. Features](#1-features) + - [2. Quick Start](#2-quick-start) + - [2.1 Quick Start for Linux and Mac](#21-quick-start-for-linux-and-mac) + - [2.2 Quick Start for Windows](#22-quick-start-for-windows) + - [3. Access the Swagger / OpenAPI](#3-access-the-swagger--openapi) + - [4. Testing APIs with Sample Requests](#4-testing-apis-with-sample-requests) + - [4.1 Testing APIs for Linux / Mac](#41-testing-apis-for-linux--mac) + - [4.2 Testing APIs for Windows](#42-testing-apis-for-windows) + - [5. Response Structure Example](#5-response-structure-example) + - [6. Deploying Your Extension](#6-deploying-your-extension) + - [7. License](#7-license) + --- ## 1. Features **Implemented** @@ -18,143 +34,70 @@ A Python FastAPI implementation that mirrors the C# `SampleExtension.Web` for Dr - `sample-entities` - `sample-entities-adaptive-card` - `samplePluginResult` (Medication Summary + Timeline cards) -- Visualization actions (Accept / Copy / Reject variants) -- Interpreter auto-detection (`scripts/start-python-dev.sh`) -- Test suite (entity extraction + composite plugin result) -- Cross-language comparison script (`scripts/compare_extensions.py`) - -**Planned** -- Auth stubs (JWT + license key) -- Structured JSON logging -- Transcript & streaming payload support -- Lint/format script flags (ruff / black) -- CI workflow (GitHub Actions .NET + Python) -- Additional provenance & reference metadata --- ## 2. Quick Start -From repository root: -```bash -./scripts/start-python-dev.sh --install # one-time dependency install -./scripts/start-python-dev.sh # start on port 5181 -PORT=5182 ./scripts/start-python-dev.sh # alternate port -``` -Swagger / OpenAPI: http://localhost:5181/docs -Run tests only: -```bash -./scripts/start-python-dev.sh --tests -``` +**Choosing Python 3.12**\ +We recommend Python 3.12 over 3.14 in this python sample extension, as a number of ML/AI (especially OSS) Python SDKs do not yet fully support 3.14, which may lead to avoidable integration issues. -Direct (advanced) uvicorn invocation: -```bash -python -m uvicorn app.main:app --host 0.0.0.0 --port 5181 --reload -``` +**Starting Directory**\ +From `pythonSampleExtension` dir: ---- -## 3. Scripts Overview -| Script | Purpose | -|--------|---------| -| `scripts/start-python-dev.sh` | Interpreter discovery, optional install, test or run server. | -| `scripts/start-csharp-dev.sh` | Launch C# sample extension (adds dotnet path if needed). | -| `scripts/compare_extensions.py` | Posts a shared payload to both services and summarizes differences. | - -Interpreter selection order: -1. Active virtual environment (`$VIRTUAL_ENV`) -2. VS Code `python.defaultInterpreterPath` -3. `python3` or `python` in PATH -4. Fallback: run `uvicorn` directly - -Flags (Python script): -``` ---install install dependencies ---tests run pytest instead of serving -PORT=XXXX override port -``` +### 2.1 Quick Start for Linux and Mac +Ensure `python3.12` is installed and can be executed from your cmd shell as `python3.12`. ---- -## 4. Comparison With C# Service -Start both (example: Python on 5182, C# on 5181): -```bash -(PORT=5182 ./scripts/start-python-dev.sh &) && ./scripts/start-csharp-dev.sh -``` -Run comparison: -```bash -python3 scripts/compare_extensions.py --payload samples/requests/note-payload.json -``` -Output shows: -- top key diff -- payload key diff -- resource counts & type sets +Run the following cmds in bash/zsh to start server. +```shell +# 1. change to the pythonSampleExtension directory +cd ./samples/DragonCopilot/Workflow/pythonSampleExtension; -When parity is complete, `notes` array is empty. +# 2. create venv, activate venv and install packages +python3.12 -m venv .venv && source .venv/bin/activate && python3.12 -m pip install --upgrade pip && python3.12 -m pip install -r requirements.txt; ---- -## 5. Response Structure Example -```jsonc -{ - "success": true, - "message": "Payload processed successfully", - "payload": { - "sample-entities": { - "schema_version": "0.1", - "resources": [ { "type": "ObservationNumber" }, { "type": "MedicalCode" } ] - }, - "sample-entities-adaptive-card": { - "schema_version": "0.1", - "resources": [ { "type": "AdaptiveCard", "adaptiveCardPayload": { "type": "AdaptiveCard", "version": "1.3" } } ] - }, - "samplePluginResult": { - "schema_version": "0.1", - "resources": [ - { "type": "AdaptiveCard", "cardTitle": "Medication Summary & Recommendations (Demo)" }, - { "type": "AdaptiveCard", "cardTitle": "Recent Clinical Entities Timeline (Demo)" } - ] - } - } -} +# 3. start server with uvicorn invocation +python3.12 -m uvicorn app.main:app --host 0.0.0.0 --port 5181 --reload ``` -Key differences vs C# naming: -- Python uses `adaptiveCardPayload` (camelCase) internally; C# serialized uses snake_case due to serializer configuration. +### 2.2 Quick Start for Windows +Ensure `python3.12` is installed over Microsoft Store and can be executed from your powershell as `python3.12`. ---- -## 6. Sectioned Clinical Note Testing -`test_clinic_note.py` programmatically splits a realistic clinic note into sections (HPI, Vitals, Labs, Exam, Impression, Plan, Follow‑up) assigning representative LOINC codes. This mirrors the shape of `samples/requests/note-payload.json` while keeping deterministic entity extraction (blood pressure, diabetes, medication keywords). - -`test_sample_plugin_result.py` validates: -- Presence of `samplePluginResult` -- Two AdaptiveCard resources (summary + timeline) -- Each card has actions & adaptive card payload -- Copy data markers (demo metadata) - -Run all tests: -```bash -pytest -q app/tests -``` +Run the following cmds in the Powershell to start server. +```powershell +# 1. change to the pythonSampleExtension directory +cd .\samples\DragonCopilot\Workflow\pythonSampleExtension; + +# 2. create venv, activate venv and install packages +python3.12 -m venv .venv && .\.venv\Scripts\activate && python3.12 -m pip install --upgrade pip && python3.12 -m pip install -r requirements.txt; -Run single test: -```bash -pytest -q app/tests/test_sample_plugin_result.py::test_sample_plugin_result_structure +# 3. start server with uvicorn invocation +python3.12 -m uvicorn app.main:app --host 0.0.0.0 --port 5181 --reload ``` +## 3 Access the Swagger / OpenAPI +After server start, you shall be able to access the python workflow sample server via Swagger / OpenAPI from your browser with the: `http://localhost:5181/docs` + --- -## 7. Sample Requests -Health: -```bash +## 4. Testing APIs with Sample Requests +After server started successfully, you can test the python workflow samples from commandline. + +### 4.1 Testing APIs for Linux / Mac +**Health API**: +```shell curl -s http://localhost:5181/health | jq curl -s http://localhost:5181/v1/health | jq ``` +**Process API**: Minimal process payload: -```bash +```shell curl -s -X POST http://localhost:5181/v1/process \ -H 'Content-Type: application/json' \ -d '{"note":{"resources":[{"content":"Patient has history of diabetes and currently taking metformin. BP recorded."}]}}' | jq ``` Include IDs: -```bash +```shell curl -s -X POST http://localhost:5181/v1/process \ -H 'Content-Type: application/json' \ -H 'x-ms-request-id: demo-req-1' \ @@ -162,55 +105,97 @@ curl -s -X POST http://localhost:5181/v1/process \ -d '{"note":{"resources":[{"content":"BP 145/98 mmHg; Diabetes risk; taking metformin"}]}}' | jq ``` ---- -## 8. Roadmap -| Area | Next Step | -|------|-----------| -| Auth | Add JWT & license-key middleware toggled by env config | -| Logging | Structured JSON (requestId, correlationId, latency) | -| Data Types | Transcript + iterative transcript stubs | -| Quality | Lint/format (`--lint`, `--format` flags) | -| CI | GitHub Actions build + test matrix (.NET / Python) | -| Testing | Additional card action + provenance tests | -| Tooling | Dependency hash cache in start script | +### 4.2 Testing APIs for Windows +**Health API**: +```powershell +Invoke-RestMethod -Uri "http://localhost:5181/health" | ConvertTo-Json -Depth 5 +Invoke-RestMethod -Uri "http://localhost:5181/v1/health" | ConvertTo-Json -Depth 5 +``` ---- -## 9. Troubleshooting -| Issue | Resolution | -|-------|------------| -| `ModuleNotFoundError: app` | Run from project root or add pyextension path to `PYTHONPATH`. | -| Port already in use | Stop prior server or run `PORT=5182 ./scripts/start-python-dev.sh`. | -| Missing entities | Ensure note text includes keywords (BP, diabetes, medication). | -| Pydantic deprecation warning | Plan migration to `ConfigDict` (safe short-term). | -| C# service not starting | Install .NET 9+ or ensure PATH; use `scripts/start-csharp-dev.sh`. | +**Process API**: +Minimal process payload: +```powershell +Invoke-RestMethod -Uri "http://localhost:5181/v1/process" -Method Post -ContentType "application/json" -Body '{"note":{"resources":[{"content":"Patient has history of diabetes and currently taking metformin. BP recorded."}]}}' | ConvertTo-Json -Depth 10 +``` ---- -## 10. Security & Compliance (Sample Caveats) -- No PHI in test inputs. -- Auth intentionally disabled – add before any real deployment. -- Logging currently minimal; not production ready. +Include IDs: +```powershell +Invoke-RestMethod -Uri "http://localhost:5181/v1/process" -Method Post -ContentType "application/json" -Headers @{"x-ms-request-id"="demo-req-1"; "x-ms-correlation-id"="demo-corr-1"} -Body '{"note":{"resources":[{"content":"BP 145/98 mmHg; Diabetes risk; taking metformin"}]}}' | ConvertTo-Json -Depth 10 +``` --- -## 11. Appendix: Manual Environment Setup -Create & activate a virtual environment manually (alternative to scripts): -```bash -python3.12 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -``` +## 5. Response Structure Example +You shall see the workflow sample server returns response similar to the following response structure. -Then run: -```bash -./scripts/start-python-dev.sh +```json +{ + "success": true, + "message": "Payload processed successfully", + "payload": { + "sample-entities": { + "schema_version": "0.1", + "resources": [ { "type": "ObservationNumber" }, { "type": "MedicalCode" } ] + }, + "sample-entities-adaptive-card": { + "schema_version": "0.1", + "resources": [ + { + "id": "card1", + "type": "AdaptiveCard", + "subtype": "note", + "adaptive_card_payload": { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "body": [] + }, + "payloadSources": [], + "dragonCopilotCopyData": "metadata_for_platform" + } + ] + }, + "samplePluginResult": { + "schema_version": "0.1", + "resources": [ + { + "id": "top1", + "type": "AdaptiveCard", + "subtype": "note", + "cardTitle": "Medication Summary & Recommendations (Demo)", + "adaptive_card_payload": { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "body": [] + }, + "payloadSources": [], + "dragonCopilotCopyData": "metadata_for_platform" + + }, + { + "id": "bottom1", + "type": "AdaptiveCard", + "subtype": "timeline", + "cardTitle": "Recent Clinical Entities Timeline (Demo)", + "adaptive_card_payload": { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "body": [] + }, + "payloadSources": [], + "dragonCopilotCopyData": "metadata_for_platform" + } + ] + } + } +} ``` +--- +## 6. Deploying Your Extension -Coverage (optional): -```bash -pip install pytest-cov -pytest --cov=app --cov-report=term-missing app/tests -``` +The steps above cover running and testing the Python sample extension locally. To fully deploy your extension—including setting up DevTunnels, packaging with the dragon-extension CLI, registering in Azure, uploading to the Dragon Admin Center, and testing inside Dragon Copilot—follow the instructions in the repository root [QUICKSTART.md](../../../../QUICKSTART.md). --- -## 12. License +## 7. License See root `LICENSE`. diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/.env b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/.env index 23be1f63..cab7e1af 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/.env +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/.env @@ -1,3 +1,6 @@ +# NOTE: These environment variables are placeholders for Azure AI Foundry integration. +# They do NOT use the DGEXT_ prefix required by Settings and are loaded separately +# by future AI agent/client code (not by pydantic-settings). # PROJECT_ENDPOINT is from the new AI Foundry (project) / Azure AI Service (new version) PROJECT_ENDPOINT="https://xxxxxx.services.ai.azure.com/api/projects/dragon-extension" MODEL_DEPLOYMENT_NAME="gpt-4.1-mini" # "gpt-4.1-mini" diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/config.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/config.py index 8b7a259b..498b9c42 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/config.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/config.py @@ -1,13 +1,12 @@ from functools import lru_cache -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="DGEXT_") + app_name: str = "Dragon Sample Extension (Python)" version: str = "0.1.0" - enable_auth: bool = False # Placeholder toggle - - class Config: - env_prefix = "DGEXT_" + # enable_auth: bool = False # Placeholder toggle — not referenced anywhere yet; uncomment when auth middleware is wired up @lru_cache def get_settings() -> Settings: diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/main.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/main.py index 0687fbc1..7e8b4e6e 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/main.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/main.py @@ -1,18 +1,19 @@ from fastapi import FastAPI, Header, HTTPException, Request -from fastapi.exceptions import RequestValidationError +# RequestValidationError import kept for reference; handler is commented out below +# from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, RedirectResponse from .models import DragonStandardPayload, ProcessResponse from .service import ProcessingService -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from .config import get_settings import logging logger = logging.getLogger("dragon.pyextension") logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s %(name)s - %(message)s") -app = FastAPI(title="Dragon Sample Extension (Python)", version="1.0.0") -service = ProcessingService() settings = get_settings() +app = FastAPI(title=settings.app_name, version=settings.version) +service = ProcessingService() @app.middleware("http") async def header_logging_middleware(request: Request, call_next): # basic structured log of tracing headers @@ -33,7 +34,7 @@ async def root_redirect(): @app.get("/health") async def root_health(): - return {"status": "healthy", "timestamp": settings.version} + return {"status": "healthy", "version": settings.version} @app.get("/v1/health") async def versioned_health(): @@ -66,12 +67,11 @@ async def process_endpoint( x_ms_correlation_id: str | None = Header(default=None, alias="x-ms-correlation-id"), ): try: - # Generate a timestamp for the incoming request using datetime.now - # set timezone to berlin time UTC+2 - start_time = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=2))) - + start_time = datetime.now(timezone.utc) logger.info("Processing incoming request at %s", start_time) resp = service.process(payload, x_ms_request_id, x_ms_correlation_id) + elapsed = datetime.now(timezone.utc) - start_time + logger.info("Request processed in %s", elapsed) return resp except HTTPException: raise diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/models.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/models.py index 97207a7e..1aba211f 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/models.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Callable -from pydantic import BaseModel, Field, ConfigDict +from typing import Any, Dict, List, Optional +from pydantic import BaseModel, Field from enum import Enum # Expanded model layer to better mirror the C# sample (not full parity but structurally closer) @@ -14,14 +14,8 @@ class ObservationValue(BaseModel): text: Optional[str] = None conceptId: Optional[str] = None -class Context(BaseModel): - id: Optional[str] = Field(None, alias="id") - contentType: Optional[str] = None - displayDescription: Optional[str] = None - class BaseResource(BaseModel): id: Optional[str] = None - context: Optional[Context] = None class MedicalCode(BaseResource): type: str = Field("MedicalCode", frozen=True) @@ -44,9 +38,7 @@ class VisualizationResource(BaseResource): type: str = Field("AdaptiveCard", frozen=True) subtype: str | None = None cardTitle: str | None = None - # adaptiveCardPayload: Any | None = None adaptive_card_payload: Any | None = None - actions: List[Dict[str, Any]] | None = None payloadSources: List[Dict[str, Any]] | None = None dragonCopilotCopyData: str | None = None partnerLogo: str | None = None @@ -56,9 +48,11 @@ class NoteResource(BaseModel): content: Optional[str] = None ## subtype of note shall be lower case 'note' -def _lower_alias(field_name: str) -> str: - """Generate lowercase aliases for JSON serialization.""" - return field_name.lower() +# _lower_alias is not currently used because the alias config on Note is +# commented out. Uncomment when alias generation is re-enabled. +# def _lower_alias(field_name: str) -> str: +# """Generate lowercase aliases for JSON serialization.""" +# return field_name.lower() class Note(BaseModel): # Ensure all fields serialize with lowercase keys (even if Python attribute had capitals) diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/service.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/service.py index 9cb76d9f..950d4a8c 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/service.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/service.py @@ -32,9 +32,13 @@ def process(self, payload: models.DragonStandardPayload, request_id: str | None, except Exception: # noqa: BLE001 logger.exception("Failed to log note model") - sample_entities, adaptive_card, composite_plugin = self._process_note(payload.note) - # response.payload["sample-entities"] = sample_entities - response.payload["dragon-predict-adaptive-card"] = adaptive_card + sample_entities, adaptive_card = self._process_note(payload.note) + response.payload["sample-entities"] = sample_entities + response.payload["adaptive-card"] = adaptive_card + # NOTE: "samplePluginResult" output is not currently supported by the + # consuming application and has been removed from the response. + # Uncomment the line below (and restore the composite logic in + # _process_note) when samplePluginResult support is re-enabled. # response.payload["samplePluginResult"] = composite_plugin logger.info("extension response:\n %s", response) @@ -74,21 +78,26 @@ def _process_note(self, note: models.Note): resources=[adaptive_card_resource], ) - # Composite plugin style result (mirrors C# samplePluginResult concept) - composite = models.DspResponse( - schema_version="0.1", - document={ - "title": note.document.get("title") if note.document else "Clinical Note Analysis", - "type": (note.document or {}).get("type") if note.document else {"text": "note"} - }, - resources=[self._composite_medication_summary(resources), self._timeline_card(resources)], - ) - return dsp_entities, adaptive_card, composite + # NOTE: Composite plugin result (samplePluginResult) is not currently + # supported by the consuming application. The composite construction + # below has been removed. To re-enable, uncomment this block and update + # _process_note to return (dsp_entities, adaptive_card, composite). + # Also uncomment _composite_medication_summary and _timeline_card below. + # + # composite = models.DspResponse( + # schema_version="0.1", + # document={ + # "title": note.document.get("title") if note.document else "Clinical Note Analysis", + # "type": (note.document or {}).get("type") if note.document else {"text": "note"} + # }, + # resources=[self._composite_medication_summary(resources), self._timeline_card(resources)], + # ) + + return dsp_entities, adaptive_card def _medical_code(self, code_value: str, description: str) -> models.MedicalCode: return models.MedicalCode( id=str(uuid4()), - context=models.Context(id="medical-code-context", contentType="medical-code", displayDescription="Medical coding for clinical entities"), code={ "identifier": code_value, "description": description, @@ -102,7 +111,6 @@ def _medical_code(self, code_value: str, description: str) -> models.MedicalCode def _vital_sign(self, value: float, unit: str) -> models.ObservationNumber: return models.ObservationNumber( id=str(uuid4()), - context=models.Context(id="vital-sign-context", contentType="vital-sign", displayDescription="Vital sign measurement"), value=value, valueUnit=unit, priority=models.Priority.High, @@ -111,122 +119,86 @@ def _vital_sign(self, value: float, unit: str) -> models.ObservationNumber: def _observation_concept(self, concept_text: str, concept_id: str) -> models.ObservationConcept: return models.ObservationConcept( id=str(uuid4()), - context=models.Context(id="observation-context", contentType="clinical-observation", displayDescription="Clinical observation finding"), value=models.ObservationValue(text=concept_text, conceptId=concept_id), priority=models.Priority.Medium, ) def _adaptive_card(self, entities: List[Any]) -> models.VisualizationResource: # Build card body similar in spirit to C# version - # body: List[Dict[str, Any]] = [ - # { - # "type": "TextBlock", - # "text": "🔍 Clinical Entities Extracted", - # "weight": "Bolder", - # "size": "Large", - # "color": "Accent", - # }, - # { - # "type": "TextBlock", - # "text": f"Found {len(entities)} clinical {'entity' if len(entities)==1 else 'entities'} in the note", - # "wrap": True, - # "size": "Medium", - # "spacing": "Small", - # }, - # ] - # if entities: - # for e in entities: - # etype = getattr(e, 'type', 'Entity') - # eid = getattr(e, 'id', '') - # body.append({ - # "type": "Container", - # "style": "emphasis", - # "spacing": "Medium", - # "items": [ - # {"type": "TextBlock", "text": f"**{etype}**", "weight": "Bolder", "size": "Medium"}, - # {"type": "TextBlock", "text": eid, "size": "Small", "wrap": True}, - # ], - # }) - # else: - # body.append({ - # "type": "Container", - # "style": "attention", - # "items": [ - # {"type": "TextBlock", "text": "ℹ️ No clinical entities were detected in this note.", "wrap": True} - # ], - # }) - # body.append({ - # "type": "TextBlock", - # "text": f"Processed at {datetime.now(timezone.utc).isoformat()}", - # "size": "Small", - # "horizontalAlignment": "Right", - # "spacing": "Medium", - # }) - - # Test structure body provided by user request - # body: List[Dict[str, Any]] = [ - # { - # "type": "ColumnSet", - # "columns": [ - # { - # "type": "Column", - # "width": "stretch", - # "items": [ - # { - # "type": "Image", - # "url": "/resources/logo_large.png", - # "altText": "conflicted dragon transparent", - # "size": "Small", - # "spacing": "None", - # }, - # { - # "type": "TextBlock", - # "text": "Clinical Risk Analysis", - # "size": "Large", - # "weight": "Bolder", - # "wrap": True, - # }, - # ], - # "verticalContentAlignment": "Center", - # } - # ], - # }, - # { - # "type": "TextBlock", - # "text": "Your risk towards high blood pressure is 87%. Your chance of developing hypertension in 3 years is 53.31%, 6 years is 70.51% and 9 years is 87.07%.", - # "wrap": True, - # }, - # ] - body: List[Dict[str, Any]] = [ { "type": "TextBlock", - "text": "Your risk towards high blood pressure is 87%. Your chance of developing hypertension in 3 years is 53.31%, 6 years is 70.51% and 9 years is 87.07%.", + "text": "🔍 Clinical Entities Extracted", + "weight": "Bolder", + "size": "Default" + }, + { + "type": "TextBlock", + "text": f"Found {len(entities)} clinical {'entity' if len(entities)==1 else 'entities'} in the note", "wrap": True, + "size": "Default", + "spacing": "Small" }, ] - + if entities: + for e in entities: + etype = getattr(e, 'type', 'Entity') + eid = getattr(e, 'id', '') + body.append({ + "type": "Container", + "style": "emphasis", + "spacing": "Medium", + "items": [ + {"type": "TextBlock", "text": f"**{etype}**", "weight": "Bolder", "size": "Default"}, + {"type": "TextBlock", "text": eid, "size": "Small", "wrap": True}, + ], + }) + else: + body.append({ + "type": "Container", + "style": "attention", + "items": [ + {"type": "TextBlock", "text": "ℹ️ No clinical entities were detected in this note.", "wrap": True} + ], + }) + body.append({ + "type": "TextBlock", + "text": f"Processed at {datetime.now(timezone.utc).isoformat()}", + "size": "Small", + "spacing": "Medium", + }) + # TODO: add extension prefix to title return models.VisualizationResource( id=str(uuid4()), - subtype="note", # hardcoded subtype - cardTitle=f"{EXTENSION_PREFIX} - Clinical Risk Scores", - # adaptiveCardPayload={ + subtype="note", + cardTitle=EXTENSION_PREFIX, adaptive_card_payload={ "type": "AdaptiveCard", - "version": "1.3", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", "body": body, - # "actions": [ - # {"type": "Action.Submit", "title": "Accept Analysis", "data": {"action": "accept"}}, - # {"type": "Action.Submit", "title": "Copy to Note", "data": {"action": "copy"}}, - # {"type": "Action.Submit", "title": "Reject Analysis", "data": {"action": "reject"}}, - # ], + "actions": [ + { + "type": "Action.Execute", + "title": "Append to note", + "verb": "appendToNoteSection", + "id": "appendToNoteSectionAction", + "data": { + "dragonAppendContent": "appended text content" + } + }, + { + "type": "Action.Execute", + "title": "Dismiss", + "verb": "reject", + "id": "rejectAction", + "data": { + "dragonExtensionToolName": "RejectCardTool" + } + } + ] }, - actions=[ - {"title": "Accept Analysis", "action": "Accept", "actionType": "Primary"}, - {"title": "Copy to Note", "action": "Copy", "actionType": "Secondary"}, - {"title": "Reject Analysis", "action": "Reject", "actionType": "Tertiary"}, - ], payloadSources=[ { "identifier": str(uuid4()), @@ -235,79 +207,105 @@ def _adaptive_card(self, entities: List[Any]) -> models.VisualizationResource: } ], dragonCopilotCopyData="Clinical entities extracted from note content", - ) - - def _composite_medication_summary(self, entities: List[Any]) -> models.VisualizationResource: - # Simplified medication summary card (parity style example) - body: List[Dict[str, Any]] = [ - { - "type": "Container", - "spacing": "Medium", - "items": [ - {"type": "TextBlock", "text": "Patient Medication Analysis", "weight": "Bolder", "size": "Large", "spacing": "None"}, - {"type": "TextBlock", "text": "Demo analysis based on detected entities", "size": "Small", "spacing": "Small", "wrap": True} - ] - } - ] - fact_items: List[Dict[str, str]] = [] - med_count = sum(1 for e in entities if getattr(e, 'type', None) == 'ObservationConcept') - condition_count = sum(1 for e in entities if getattr(e, 'type', None) == 'MedicalCode') - vital_count = sum(1 for e in entities if getattr(e, 'type', None) == 'ObservationNumber') - fact_items.append({"title": "Medications Detected:", "value": f"{med_count}"}) - fact_items.append({"title": "Conditions Detected:", "value": f"{condition_count}"}) - fact_items.append({"title": "Vitals Detected:", "value": f"{vital_count}"}) - body.append({ - "type": "FactSet", - "facts": fact_items - }) - - return models.VisualizationResource( - id=str(uuid4()), - subtype="Note", - cardTitle="Medication Summary & Recommendations (Demo)", partnerLogo="https://contoso.com/logo.png", - adaptiveCardPayload={ - "type": "AdaptiveCard", - "version": "1.3", - "body": body - }, - actions=[ - {"title": "Accept Recommendations", "action": "accept", "actionType": "primary"}, - {"title": "Reject Changes", "action": "reject", "actionType": "secondary"}, - {"title": "Copy Full Analysis", "action": "copy", "actionType": "tertiary", "code": "DEMO ANALYSIS\nEntities summary included."} - ], - payloadSources=[ - {"identifier": str(uuid4()), "description": "Python Demo Medication Analysis Service", "url": "http://localhost/v1/process"} - ], - dragonCopilotCopyData="medication_analysis|demo:1|generated:" + datetime.now(timezone.utc).isoformat() + references=[], ) - def _timeline_card(self, entities: List[Any]) -> models.VisualizationResource: - # Simplified timeline card - body: List[Dict[str, Any]] = [ - { - "type": "Container", - "items": [ - {"type": "TextBlock", "text": "Lab / Clinical Trend Analysis (Demo)", "weight": "Bolder", "size": "Medium"}, - {"type": "TextBlock", "text": f"Detected {len(entities)} entities to date", "size": "Small", "spacing": "Small"} - ] - } - ] - return models.VisualizationResource( - id=str(uuid4()), - subtype="timeline", - cardTitle="Recent Clinical Entities Timeline (Demo)", - adaptiveCardPayload={ - "type": "AdaptiveCard", - "version": "1.3", - "body": body - }, - actions=[ - {"title": "View Full Results", "action": "accept", "actionType": "primary"}, - {"title": "Copy Timeline Data", "action": "copy", "actionType": "tertiary", "code": "TIMELINE DEMO\nNo real data."} - ], - payloadSources=[ - {"identifier": str(uuid4()), "description": "Python Demo Timeline Service", "url": "http://localhost/v1/process"} - ], - dragonCopilotCopyData="lab_timeline|demo:1|generated:" + datetime.now(timezone.utc).isoformat() - ) + # NOTE: _composite_medication_summary and _timeline_card are not currently + # used because the samplePluginResult output is not supported by the + # consuming application. Uncomment these methods (and the composite + # construction in _process_note) when samplePluginResult support is + # re-enabled. + # + # def _composite_medication_summary(self, entities: List[Any]) -> models.VisualizationResource: + # # Simplified medication summary card (parity style example) + # body: List[Dict[str, Any]] = [ + # { + # "type": "Container", + # "spacing": "Medium", + # "items": [ + # {"type": "TextBlock", "text": "Patient Medication Analysis", "weight": "Bolder", "size": "Default", "spacing": "None"}, + # {"type": "TextBlock", "text": "Demo analysis based on detected entities", "size": "Small", "spacing": "Small", "wrap": True} + # ] + # } + # ] + # fact_items: List[Dict[str, str]] = [] + # med_count = sum(1 for e in entities if getattr(e, 'type', None) == 'ObservationConcept') + # condition_count = sum(1 for e in entities if getattr(e, 'type', None) == 'MedicalCode') + # vital_count = sum(1 for e in entities if getattr(e, 'type', None) == 'ObservationNumber') + # fact_items.append({"title": "Medications Detected:", "value": f"{med_count}"}) + # fact_items.append({"title": "Conditions Detected:", "value": f"{condition_count}"}) + # fact_items.append({"title": "Vitals Detected:", "value": f"{vital_count}"}) + # body.append({ + # "type": "FactSet", + # "facts": fact_items + # }) + # + # return models.VisualizationResource( + # id=str(uuid4()), + # subtype="note", + # cardTitle="Medication Summary & Recommendations (Demo)", + # adaptive_card_payload={ + # "type": "AdaptiveCard", + # "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + # "version": "1.6", + # "body": body, + # "actions": [ + # { + # "type": "Action.Execute", + # "title": "Dismiss", + # "verb": "reject", + # "id": "rejectAction", + # "data": { + # "dragonExtensionToolName": "RejectCardTool" + # } + # } + # ] + # }, + # payloadSources=[ + # {"identifier": str(uuid4()), "description": "Python Demo Medication Analysis Service", "url": "http://localhost/v1/process"} + # ], + # dragonCopilotCopyData="medication_analysis|demo:1|generated:" + datetime.now(timezone.utc).isoformat(), + # references=[], + # partnerLogo="https://contoso.com/logo.png", + # ) + # + # def _timeline_card(self, entities: List[Any]) -> models.VisualizationResource: + # # Simplified timeline card + # body: List[Dict[str, Any]] = [ + # { + # "type": "Container", + # "items": [ + # {"type": "TextBlock", "text": "Lab / Clinical Trend Analysis (Demo)", "weight": "Bolder", "size": "Default"}, + # {"type": "TextBlock", "text": f"Detected {len(entities)} entities to date", "size": "Small", "spacing": "Small"} + # ] + # } + # ] + # return models.VisualizationResource( + # id=str(uuid4()), + # subtype="timeline", + # cardTitle="Recent Clinical Entities Timeline (Demo)", + # adaptive_card_payload={ + # "type": "AdaptiveCard", + # "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + # "version": "1.6", + # "body": body, + # "actions": [ + # { + # "type": "Action.Execute", + # "title": "Dismiss", + # "verb": "reject", + # "id": "rejectAction", + # "data": { + # "dragonExtensionToolName": "RejectCardTool" + # } + # } + # ] + # }, + # payloadSources=[ + # {"identifier": str(uuid4()), "description": "Python Demo Timeline Service", "url": "http://localhost/v1/process"} + # ], + # dragonCopilotCopyData="lab_timeline|demo:1|generated:" + datetime.now(timezone.utc).isoformat(), + # partnerLogo="https://contoso.com/logo.png", + # references=[], + # ) \ No newline at end of file diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/conftest.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/conftest.py new file mode 100644 index 00000000..d8b97c36 --- /dev/null +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/conftest.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +# Ensure the pyextension root (parent of the 'app' package) is on sys.path so 'app' can be imported +CURRENT_FILE = Path(__file__).resolve() +PYEXT_ROOT = CURRENT_FILE.parents[2] # .../pyextension +if str(PYEXT_ROOT) not in sys.path: + sys.path.insert(0, str(PYEXT_ROOT)) + +from app.main import app # type: ignore # noqa: E402 + + +@pytest.fixture() +def client(): + """Shared FastAPI TestClient fixture.""" + return TestClient(app) diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_adaptive_card.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_adaptive_card.py index d66444fa..5e78d991 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_adaptive_card.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_adaptive_card.py @@ -1,16 +1,3 @@ -import sys -from pathlib import Path -from fastapi.testclient import TestClient - -CURRENT_FILE = Path(__file__).resolve() -PYEXT_ROOT = CURRENT_FILE.parents[2] -if str(PYEXT_ROOT) not in sys.path: - sys.path.insert(0, str(PYEXT_ROOT)) - -from app.main import app # type: ignore - -client = TestClient(app) - # Minimal note payload that triggers at least one entity (blood pressure) so counts are deterministic NOTE_CONTENT = "BP: 145/98 mmHg Patient denies chest pain. Diabetes risk evaluated. Medication review done." @@ -22,45 +9,42 @@ } } -def test_sample_plugin_result_structure(): +# Disabled: samplePluginResult is no longer included in the response payload +# because it caused an extra section rejected by the final application. + + +def test_adaptive_card_structure(client): + """Validate the adaptive-card payload key and its AdaptiveCard content.""" resp = client.post("/v1/process", json=payload) assert resp.status_code == 200 body = resp.json() assert body["success"] is True - plugin = body["payload"].get("samplePluginResult") - assert plugin is not None, "samplePluginResult missing from payload" - assert isinstance(plugin, dict) - resources = plugin.get("resources") or [] - assert len(resources) == 2, "Expected 2 resources in samplePluginResult (summary + timeline)" - - # Validate both are AdaptiveCards - types = [r.get("type") for r in resources if isinstance(r, dict)] - assert all(t == "AdaptiveCard" for t in types), f"Unexpected resource types: {types}" - - # Identify cards by titles - titles = {r.get("cardTitle"): r for r in resources if isinstance(r, dict)} - summary_card = next((r for r in resources if "Medication" in (r.get("cardTitle") or "")), None) - timeline_card = next((r for r in resources if "Timeline" in (r.get("cardTitle") or "")), None) - assert summary_card is not None, "Medication summary card not found" - assert timeline_card is not None, "Timeline card not found" - - # Basic action validation (at least one action per card) - for card in (summary_card, timeline_card): - actions = card.get("actions") or [] - assert len(actions) >= 1, f"Card {card.get('cardTitle')} missing actions" - - # Ensure adaptive card payload present - for card in (summary_card, timeline_card): - # Python model uses camelCase adaptiveCardPayload - assert "adaptiveCardPayload" in card, f"Adaptive card payload missing for {card.get('cardTitle')}" - ac = card["adaptiveCardPayload"] - assert isinstance(ac, dict) - assert ac.get("type") == "AdaptiveCard" - assert ac.get("version") in {"1.3", "1.2", "1.4"} - # Validate dragonCopilotCopyData has a recognizable pattern - for card in (summary_card, timeline_card): - dccd = card.get("dragonCopilotCopyData", "") - assert dccd, "dragonCopilotCopyData missing" - assert "demo:1" in dccd or "generated:" in dccd + # Payload should contain 'sample-entities' and 'adaptive-card' (not 'samplePluginResult') + assert "adaptive-card" in body["payload"], "adaptive-card key missing from payload" + assert "samplePluginResult" not in body["payload"], "samplePluginResult should not be present" + + adaptive_wrapper = body["payload"]["adaptive-card"] + assert isinstance(adaptive_wrapper, dict) + resources = adaptive_wrapper.get("resources") or [] + assert len(resources) == 1, "Expected 1 resource in adaptive-card" + + card = resources[0] + assert card.get("type") == "AdaptiveCard" + assert card.get("subtype") == "note" + + # Validate adaptive card payload + assert "adaptive_card_payload" in card, "adaptive_card_payload missing" + ac = card["adaptive_card_payload"] + assert isinstance(ac, dict) + assert ac.get("type") == "AdaptiveCard" + assert ac.get("version") == "1.6" + + # Validate actions exist + actions = ac.get("actions") or [] + assert len(actions) >= 1, "Card missing actions" + + # Validate metadata fields + assert card.get("dragonCopilotCopyData"), "dragonCopilotCopyData missing" + assert card.get("payloadSources"), "payloadSources missing" diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_clinic_note.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_clinic_note.py index fb6495c1..84d12645 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_clinic_note.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_clinic_note.py @@ -1,17 +1,3 @@ -import sys -from pathlib import Path -from fastapi.testclient import TestClient - -# Ensure import path -CURRENT_FILE = Path(__file__).resolve() -PYEXT_ROOT = CURRENT_FILE.parents[2] -if str(PYEXT_ROOT) not in sys.path: - sys.path.insert(0, str(PYEXT_ROOT)) - -from app.main import app # type: ignore - -client = TestClient(app) - CLINIC_NOTE = """Clinic Note\nPatient Name: John Doe\nDate of Note: 09/09/2025\nLocation: Haddox Medi-clinic \n\nSubjective:\nPatient is a 65-year-old white male here for evaluation of elevated blood pressure and fatigue. No prior diagnosis of hypertension or diabetes. Family history includes hypertension and diabetes in both parents. Patient reports sedentary lifestyle and BMI is elevated.\n\nObjective:\n\nVitals:\nBP: 145/98 mmHg\nHR: 78 bpm\nTemp: 98.6°F\nRR: 16\nSpO₂: 98% RA\n\nHeight: 5'10"\nWeight: 210 lbs\nBMI: 30.1 kg/m²\n\nLabs:\nFasting glucose: 108 mg/dL\nHbA1c: 5.9%\nLipid panel: Total cholesterol 210 mg/dL, LDL 140 mg/dL, HDL 42 mg/dL, Triglycerides 180 mg/dL\n\nPhysical Examination:\nGeneral: Well-nourished, well-developed male in no acute distress\nHEENT: Normocephalic, atraumatic, PERRLA, EOMI, oropharynx clear\nNeck: Supple, no lymphadenopathy or thyromegaly\nCardiovascular: Regular rate and rhythm, no murmurs, rubs, or gallops\nRespiratory: Clear to auscultation bilaterally, no wheezes or rales\nAbdomen: Soft, non-tender, no hepatosplenomegaly\nGenitourinary: Normal external genitalia, no CVA tenderness\nExtremities: No edema, pulses intact\nNeurological: Alert and oriented x3, normal gait\nSkin: Warm, dry, intact\n\nImpression:\nHypertension\n\nPlan:\nHypertension:\nLifestyle counseling: DASH diet, physical activity.\nConsider outpatient follow-up for repeat BP and possible initiation of antihypertensives if persistently elevated.\n\nDiabetes:\nRepeat fasting glucose and HbA1c in 3 months.\nRefer to outpatient diabetes prevention program.\nEncourage weight loss and increased physical activity.\n\nFollow-up:\nPCP visit in 1–2 weeks.\nNutritionist referral.""" @@ -56,24 +42,12 @@ def _extract_sections(note: str): def _build_structured_resources(): - resources = [] - for idx, (key, desc, content, code) in enumerate(_extract_sections(CLINIC_NOTE)): - resources.append({ - "legacy_id": key.lower().replace(' ', '_'), - "context": { - "content_type": "document_section", - "display_description": desc, - "spoken_forms": [desc.lower(), key.lower(), key], - "codes": [ - {"system": "loinc.org", "identifier": code, "system_url": "http://loinc.org"} - ] - }, - "content": content - }) - return resources + # NoteResource only accepts 'content'; section metadata (key, desc, code) + # from _extract_sections is not used in the resource payload. + return [{"content": content} for _, _, content, _ in _extract_sections(CLINIC_NOTE)] -def test_clinic_note_entity_extraction(): +def test_clinic_note_entity_extraction(client): # Structured payload modeled after samples/requests/note-payload.json but minimized for test speed resources = _build_structured_resources() # Ensure at least one resource retains BP reading for entity extraction @@ -109,7 +83,8 @@ def test_clinic_note_entity_extraction(): types = {e.get("type") for e in entities if isinstance(e, dict)} assert "ObservationNumber" in types # blood pressure assert "MedicalCode" in types or "ObservationConcept" in types # diabetes indicators - adaptive_wrapper = body["payload"].get("sample-entities-adaptive-card") + # adaptive-card key replaced sample-entities-adaptive-card to avoid errors in final application + adaptive_wrapper = body["payload"].get("adaptive-card") assert adaptive_wrapper is not None # Unwrap DSP response structure -> resources[0] should be the actual adaptive card visualization resource if isinstance(adaptive_wrapper, dict) and "resources" in adaptive_wrapper: diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_process.py b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_process.py index d48d5b1a..2bb6ceae 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_process.py +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/app/tests/test_process.py @@ -1,30 +1,16 @@ -import sys -from pathlib import Path - -# Ensure the pyextension root (parent of the 'app' package) is on sys.path so 'app' can be imported -CURRENT_FILE = Path(__file__).resolve() -PYEXT_ROOT = CURRENT_FILE.parents[2] # .../pyextension -if str(PYEXT_ROOT) not in sys.path: - sys.path.insert(0, str(PYEXT_ROOT)) - -from fastapi.testclient import TestClient # type: ignore -from app.main import app # type: ignore - -client = TestClient(app) - -def test_health(): +def test_health(client): r = client.get("/health") assert r.status_code == 200 assert r.json()["status"] == "healthy" -def test_v1_health(): +def test_v1_health(client): r = client.get("/v1/health") assert r.status_code == 200 body = r.json() assert body["status"] == "healthy" assert "/v1/process" in body["endpoints"]["process"] -def test_process_empty_payload(): +def test_process_empty_payload(client): r = client.post("/v1/process", json={}) assert r.status_code == 200 data = r.json() diff --git a/samples/DragonCopilot/Workflow/pythonSampleExtension/requirements.txt b/samples/DragonCopilot/Workflow/pythonSampleExtension/requirements.txt index 75dc6d3b..faca2701 100644 --- a/samples/DragonCopilot/Workflow/pythonSampleExtension/requirements.txt +++ b/samples/DragonCopilot/Workflow/pythonSampleExtension/requirements.txt @@ -1,6 +1,5 @@ fastapi==0.116.1 uvicorn==0.35.0 pydantic==2.11.7 -httpx==0.28.1 pytest==8.4.2 pydantic-settings==2.10.1 \ No newline at end of file diff --git a/samples/audio-recordings/audio_cardiology.mp3 b/samples/audio-recordings/audio_cardiology.mp3 new file mode 100644 index 00000000..1c7870eb Binary files /dev/null and b/samples/audio-recordings/audio_cardiology.mp3 differ diff --git a/samples/audio-recordings/audio_gi.mp3 b/samples/audio-recordings/audio_gi.mp3 new file mode 100644 index 00000000..6a70705e Binary files /dev/null and b/samples/audio-recordings/audio_gi.mp3 differ diff --git a/samples/audio-recordings/audio_internal_medicine.mp3 b/samples/audio-recordings/audio_internal_medicine.mp3 new file mode 100644 index 00000000..8dc68986 Binary files /dev/null and b/samples/audio-recordings/audio_internal_medicine.mp3 differ diff --git a/samples/audio-recordings/audio_oncology.mp3 b/samples/audio-recordings/audio_oncology.mp3 new file mode 100644 index 00000000..ce2b5aa7 Binary files /dev/null and b/samples/audio-recordings/audio_oncology.mp3 differ diff --git a/samples/audio-recordings/audio_psychiatry.mp3 b/samples/audio-recordings/audio_psychiatry.mp3 new file mode 100644 index 00000000..468de81f Binary files /dev/null and b/samples/audio-recordings/audio_psychiatry.mp3 differ diff --git a/src/Dragon.Copilot.Models/ActionButtonType.cs b/src/Dragon.Copilot.Models/ActionButtonType.cs index d4f85219..af0ec9ef 100644 --- a/src/Dragon.Copilot.Models/ActionButtonType.cs +++ b/src/Dragon.Copilot.Models/ActionButtonType.cs @@ -12,17 +12,22 @@ namespace Dragon.Copilot.Models; public enum ActionButtonType { /// - /// Primary action button style + /// Accept action button /// - Primary, + Accept, /// - /// Secondary action button style + /// Copy action button /// - Secondary, + Copy, /// - /// Tertiary action button style + /// Reject action button /// - Tertiary + Reject, + + /// + /// Update note action button + /// + UpdateNote } diff --git a/src/Dragon.Copilot.Models/AdaptiveCardPayload.cs b/src/Dragon.Copilot.Models/AdaptiveCardPayload.cs new file mode 100644 index 00000000..2e4cd705 --- /dev/null +++ b/src/Dragon.Copilot.Models/AdaptiveCardPayload.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Dragon.Copilot.Models; + +/// +/// Adaptive card payload structure +/// +public class AdaptiveCardPayload +{ + /// + /// Adaptive card schema URL + /// + [JsonPropertyName("$schema")] + public string Schema {get; } = "http://adaptivecards.io/schemas/adaptive-card.json"; + + /// + /// Type of the card (always "AdaptiveCard") + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "AdaptiveCard"; + + /// + /// Adaptive card version + /// + [JsonPropertyName("version")] + public string Version { get; init; } = "1.3"; + + /// + /// Body elements of the adaptive card + /// + [JsonPropertyName("body")] + public IList Body { get; init; } = new List(); +} \ No newline at end of file diff --git a/src/Dragon.Copilot.Models/VisualizationResource.cs b/src/Dragon.Copilot.Models/VisualizationResource.cs index f81a41fb..a81cf7d0 100644 --- a/src/Dragon.Copilot.Models/VisualizationResource.cs +++ b/src/Dragon.Copilot.Models/VisualizationResource.cs @@ -45,7 +45,7 @@ public class VisualizationResource : IResource /// Adaptive card payload (required) /// [JsonPropertyName("adaptive_card_payload")] - public required object AdaptiveCardPayload { get; set; } + public required AdaptiveCardPayload AdaptiveCardPayload { get; set; } /// /// Available actions for the card diff --git a/tools/dragon-extension-cli/package-lock.json b/tools/dragon-extension-cli/package-lock.json index 22e7a990..45336040 100644 --- a/tools/dragon-extension-cli/package-lock.json +++ b/tools/dragon-extension-cli/package-lock.json @@ -9,27 +9,27 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^7.9.0", + "@inquirer/prompts": "^7.10.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "archiver": "^7.0.1", "chalk": "^5.6.2", "commander": "^14.0.2", "fs-extra": "^11.3.2", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.1" }, "bin": { "dragon-extension": "dist/cli.js" }, "devDependencies": { - "@types/archiver": "^6.0.2", + "@types/archiver": "^7.0.0", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^24.10.0", "jest": "^30.2.0", "pkg": "^5.8.1", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "ts-jest": "^29.4.5", "ts-node": "^10.9.2", "typescript": "^5.9.3" @@ -607,25 +607,25 @@ } }, "node_modules/@inquirer/ansi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", - "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/checkbox": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.0.tgz", - "integrity": "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.1.tgz", + "integrity": "sha512-rOcLotrptYIy59SGQhKlU0xBg1vvcVl2FdPIEclUvKHh0wo12OfGkId/01PIMJ/V+EimJ77t085YabgnQHBa5A==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/core": "^10.3.0", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.1", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -640,13 +640,13 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", - "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.20.tgz", + "integrity": "sha512-HDGiWh2tyRZa0M1ZnEIUCQro25gW/mN8ODByicQrbR1yHx4hT+IOpozCMi5TgBtUdklLwRI2mv14eNpftDluEw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -661,19 +661,19 @@ } }, "node_modules/@inquirer/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", - "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz", + "integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", + "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -700,14 +700,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.21", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", - "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.22.tgz", + "integrity": "sha512-8yYZ9TCbBKoBkzHtVNMF6PV1RJEUvMlhvmS3GxH4UvXMEHlS45jFyqFy0DU+K42jBs5slOaA78xGqqqWAx3u6A==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/external-editor": "^1.0.2", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.1", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -722,14 +722,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.21.tgz", - "integrity": "sha512-+mScLhIcbPFmuvU3tAGBed78XvYHSvCl6dBiYMlzCLhpr0bzGzd8tfivMMeqND6XZiaZ1tgusbUHJEfc6YzOdA==", + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.22.tgz", + "integrity": "sha512-9XOjCjvioLjwlq4S4yXzhvBmAXj5tG+jvva0uqedEsQ9VD8kZ+YT7ap23i0bIXOtow+di4+u3i6u26nDqEfY4Q==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -744,12 +744,12 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "license": "MIT", "dependencies": { - "chardet": "^2.1.0", + "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "engines": { @@ -765,22 +765,22 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", - "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/input": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.5.tgz", - "integrity": "sha512-7GoWev7P6s7t0oJbenH0eQ0ThNdDJbEAEtVt9vsrYZ9FulIokvd823yLyhQlWHJPGce1wzP53ttfdCZmonMHyA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.0.tgz", + "integrity": "sha512-h4fgse5zeGsBSW3cRQqu9a99OXRdRsNCvHoBqVmz40cjYjYFzcfwD0KA96BHIPlT7rZw0IpiefQIqXrjbzjS4Q==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -795,13 +795,13 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.21.tgz", - "integrity": "sha512-5QWs0KGaNMlhbdhOSCFfKsW+/dcAVC2g4wT/z2MCiZM47uLgatC5N20kpkDQf7dHx+XFct/MJvvNGy6aYJn4Pw==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.22.tgz", + "integrity": "sha512-oAdMJXz++fX58HsIEYmvuf5EdE8CfBHHXjoi9cTcQzgFoHGZE+8+Y3P38MlaRMeBvAVnkWtAxMUF6urL2zYsbg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -816,14 +816,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.21.tgz", - "integrity": "sha512-xxeW1V5SbNFNig2pLfetsDb0svWlKuhmr7MPJZMYuDnCTkpVBI+X/doudg4pznc1/U+yYmWFFOi4hNvGgUo7EA==", + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.22.tgz", + "integrity": "sha512-CbdqK1ioIr0Y3akx03k/+Twf+KSlHjn05hBL+rmubMll7PsDTGH0R4vfFkr+XrkB0FOHrjIwVP9crt49dgt+1g==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -838,21 +838,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", - "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.0.tgz", + "integrity": "sha512-X2HAjY9BClfFkJ2RP3iIiFxlct5JJVdaYYXhA7RKxsbc9KL+VbId79PSoUGH/OLS011NFbHHDMDcBKUj3T89+Q==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.0", - "@inquirer/confirm": "^5.1.19", - "@inquirer/editor": "^4.2.21", - "@inquirer/expand": "^4.0.21", - "@inquirer/input": "^4.2.5", - "@inquirer/number": "^3.0.21", - "@inquirer/password": "^4.0.21", - "@inquirer/rawlist": "^4.1.9", - "@inquirer/search": "^3.2.0", - "@inquirer/select": "^4.4.0" + "@inquirer/checkbox": "^4.3.1", + "@inquirer/confirm": "^5.1.20", + "@inquirer/editor": "^4.2.22", + "@inquirer/expand": "^4.0.22", + "@inquirer/input": "^4.3.0", + "@inquirer/number": "^3.0.22", + "@inquirer/password": "^4.0.22", + "@inquirer/rawlist": "^4.1.10", + "@inquirer/search": "^3.2.1", + "@inquirer/select": "^4.4.1" }, "engines": { "node": ">=18" @@ -867,14 +867,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.9.tgz", - "integrity": "sha512-AWpxB7MuJrRiSfTKGJ7Y68imYt8P9N3Gaa7ySdkFj1iWjr6WfbGAhdZvw/UnhFXTHITJzxGUI9k8IX7akAEBCg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.10.tgz", + "integrity": "sha512-Du4uidsgTMkoH5izgpfyauTL/ItVHOLsVdcY+wGeoGaG56BV+/JfmyoQGniyhegrDzXpfn3D+LFHaxMDRygcAw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -889,15 +889,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.0.tgz", - "integrity": "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.1.tgz", + "integrity": "sha512-cKiuUvETublmTmaOneEermfG2tI9ABpb7fW/LqzZAnSv4ZaJnbEis05lOkiBuYX5hNdnX0Q9ryOQyrNidb55WA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.1", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -912,16 +912,16 @@ } }, "node_modules/@inquirer/select": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.0.tgz", - "integrity": "sha512-kaC3FHsJZvVyIjYBs5Ih8y8Bj4P/QItQWrZW22WJax7zTN+ZPXVGuOM55vzbdCP9zKUiBd9iEJVdesujfF+cAA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.1.tgz", + "integrity": "sha512-E9hbLU4XsNe2SAOSsFrtYtYQDVi1mfbqJrPDvXKnGlnRiApBdWMJz7r3J2Ff38AqULkPUD3XjQMD4492TymD7Q==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/core": "^10.3.0", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.1", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -936,9 +936,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", - "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "license": "MIT", "engines": { "node": ">=18" @@ -1099,9 +1099,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1842,9 +1842,9 @@ } }, "node_modules/@types/archiver": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz", - "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", "dev": true, "license": "MIT", "dependencies": { @@ -2939,9 +2939,9 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, "node_modules/chownr": { @@ -3792,9 +3792,9 @@ "license": "MIT" }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -5341,9 +5341,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5669,12 +5669,12 @@ } }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/napi-build-utils": { @@ -6568,13 +6568,13 @@ } }, "node_modules/rimraf": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.0.tgz", - "integrity": "sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.3", + "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -6588,38 +6588,16 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, "engines": { "node": "20 || >=22" }, @@ -6628,21 +6606,21 @@ } }, "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -6654,9 +6632,9 @@ } }, "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/tools/dragon-extension-cli/package.json b/tools/dragon-extension-cli/package.json index b90c8474..1663b58a 100644 --- a/tools/dragon-extension-cli/package.json +++ b/tools/dragon-extension-cli/package.json @@ -35,24 +35,24 @@ "author": "", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^7.9.0", + "@inquirer/prompts": "^7.10.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "archiver": "^7.0.1", "chalk": "^5.6.2", "commander": "^14.0.2", "fs-extra": "^11.3.2", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.1" }, "devDependencies": { - "@types/archiver": "^6.0.2", + "@types/archiver": "^7.0.0", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^24.10.0", "jest": "^30.2.0", "pkg": "^5.8.1", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "ts-jest": "^29.4.5", "ts-node": "^10.9.2", "typescript": "^5.9.3"