Skip to content

Commit 096abae

Browse files
JhontSouthrido-min
andauthored
Add copilotstudio client sample (#67)
* add copilotstudio client sample * Add Copilot Studio Client sample with updated configuration and new implementation * Update README for Copilot Studio Client sample to clarify usage of the agents-copilotstudio-client package * Add build step for Copilot Studio client sample in CI workflow * Update dependencies in package.json for Copilot Studio Client sample --------- Co-authored-by: rido <rmpablos@microsoft.com> Co-authored-by: Rido <rido-min@users.noreply.github.com>
1 parent 55f81e2 commit 096abae

File tree

8 files changed

+285
-2
lines changed

8 files changed

+285
-2
lines changed

.github/workflows/ci-node.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ jobs:
2121

2222
- name: Build echo bot
2323
working-directory: ./samples/basic/echo-bot/nodejs/
24+
run: |
25+
npm install
26+
npm run build
27+
28+
- name: Build Copilot Studio client sample
29+
working-directory: ./samples/basic/copilotstudio-client/nodejs/
2430
run: |
2531
npm install
2632
npm run build

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ FodyWeavers.xsd
399399

400400
#Ignore env files
401401
*.env
402-
403402
dist/
404403
devTools/
405-
node_modules/
404+
node_modules/
405+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
registry=https://registry.npmjs.org/
2+
@microsoft:registry=https://pkgs.dev.azure.com/ConversationalAI/BotFramework/_packaging/SDK/npm/registry/
3+
package-lock=false
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copilot Studio Client
2+
3+
This is a sample to show how to use the `@microsoft/agents-copilotstudio-client` package to talk to an Agent hosted in CopilotStudio.
4+
5+
6+
## Prerequisite
7+
8+
To set up this sample, you will need the following:
9+
10+
1. [Node.js](https://nodejs.org) version 20 or higher
11+
12+
```bash
13+
# determine node version
14+
node --version
15+
```
16+
2. An Agent Created in Microsoft Copilot Studio or access to an existing Agent.
17+
3. Ability to Create an Application Identity in Azure for a Public Client/Native App Registration Or access to an existing Public Client/Native App registration with the CopilotStudio.Copilot.Invoke API Permission assigned.
18+
19+
## Create an Agent in Copilot Studio
20+
21+
1. Create an Agent in [Copilot Studio](https://copilotstudio.microsoft.com)
22+
1. Publish your newly created Copilot
23+
2. Goto Settings => Advanced => Metadata and copy the following values, You will need them later:
24+
1. Schema name
25+
2. Environment Id
26+
27+
## Create an Application Registration in Entra ID
28+
29+
This step will require permissions to create application identities in your Azure tenant. For this sample, you will create a Native Client Application Identity, which does not have secrets.
30+
31+
1. Open https://portal.azure.com
32+
2. Navigate to Entra Id
33+
3. Create a new App Registration in Entra ID
34+
1. Provide a Name
35+
2. Choose "Accounts in this organization directory only"
36+
3. In the "Select a Platform" list, Choose "Public Client/native (mobile & desktop)
37+
4. In the Redirect URI url box, type in `http://localhost` (**note: use HTTP, not HTTPS**)
38+
5. Then click register.
39+
4. In your newly created application
40+
1. On the Overview page, Note down for use later when configuring the example application:
41+
1. The Application (client) ID
42+
2. The Directory (tenant) ID
43+
2. Go to API Permissions in `Manage` section
44+
3. Click Add Permission
45+
1. In the side panel that appears, Click the tab `API's my organization uses`
46+
2. Search for `Power Platform API`.
47+
1. *If you do not see `Power Platform API` see the note at the bottom of this section.*
48+
3. In the permissions list, choose `CopilotStudio` and Check `CopilotStudio.Copilots.Invoke`
49+
4. Click `Add Permissions`
50+
4. (Optional) Click `Grant Admin consent for copilotsdk`
51+
52+
> [!TIP]
53+
> If you do not see `Power Platform API` in the list of API's your organization uses, you need to add the Power Platform API to your tenant. To do that, goto [Power Platform API Authentication](https://learn.microsoft.com/power-platform/admin/programmability-authentication-v2#step-2-configure-api-permissions) and follow the instructions on Step 2 to add the Power Platform Admin API to your Tenant
54+
55+
## Instructions - Configure the Example Application
56+
57+
With the above information, you can now run the client `CopilostStudioClient` sample.
58+
59+
1. Open the `env.TEMPLATE` file and rename it to `.env`.
60+
2. Configure the values based on what was recorded during the setup phase.
61+
62+
```bash
63+
environmentId="" # Environment ID of environment with the CopilotStudio App.
64+
botIdentifier="" # Schema Name of the Copilot to use
65+
tenantId="" # Tenant ID of the App Registration used to login, this should be in the same tenant as the Copilot.
66+
appClientId="" # App ID of the App Registration used to login, this should be in the same tenant as the Copilot.
67+
```
68+
69+
3. Run the CopilotStudioClient sample using `npm start`, which will install the packages, build the project and run it.
70+
71+
This should challenge you to login and connect to the Copilot Studio Hosted bot, allowing you to communicate via a console interface.
72+
73+
## Authentication
74+
75+
The DirectToEngine Client requires a User Token to operate. For this sample, we are using a user interactive flow to get the user token for the application ID created above.
76+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "copilotstudio-client",
3+
"version": "1.0.0",
4+
"private": true,
5+
"description": "Agents Copilot Studio Client sample",
6+
"author": "Microsoft",
7+
"license": "MIT",
8+
"main": "./dist/index.js",
9+
"type": "module",
10+
"scripts": {
11+
"build": "tsc --build",
12+
"prestart": "npm run build",
13+
"start": "node --env-file .env ./dist/index.js"
14+
},
15+
"dependencies": {
16+
"@microsoft/agents-copilotstudio-client": "0.1.20",
17+
"@azure/msal-node": "^3.2.3",
18+
"open": "^10.1.0"
19+
},
20+
"devDependencies": {
21+
"@types/debug": "^4.1.12",
22+
"@types/node": "^22.13.4",
23+
"typescript": "^5.7.3"
24+
},
25+
"keywords": []
26+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import * as msal from '@azure/msal-node'
7+
import { Activity, ActivityTypes, CardAction } from '@microsoft/agents-bot-activity'
8+
import { ConnectionSettings, loadCopilotStudioConnectionSettingsFromEnv, CopilotStudioClient } from '@microsoft/agents-copilotstudio-client'
9+
import pkg from '@microsoft/agents-copilotstudio-client/package.json' with { type: 'json' }
10+
import readline from 'readline'
11+
import open from 'open'
12+
import os from 'os'
13+
import path from 'path'
14+
15+
import { MsalCachePlugin } from './msalCachePlugin.js'
16+
17+
async function acquireToken(settings: ConnectionSettings): Promise<string> {
18+
const msalConfig = {
19+
auth: {
20+
clientId: settings.appClientId,
21+
authority: `https://login.microsoftonline.com/${settings.tenantId}`,
22+
},
23+
cache: {
24+
cachePlugin: new MsalCachePlugin(path.join(os.tmpdir(), 'msal.usercache.json'))
25+
},
26+
system: {
27+
loggerOptions: {
28+
loggerCallback(loglevel: msal.LogLevel, message: string, containsPii: boolean) {
29+
console.log(message)
30+
},
31+
piiLoggingEnabled: false,
32+
logLevel: msal.LogLevel.Verbose,
33+
}
34+
}
35+
}
36+
const pca = new msal.PublicClientApplication(msalConfig)
37+
const tokenRequest = {
38+
scopes: ['https://api.powerplatform.com/.default'],
39+
redirectUri: 'http://localhost',
40+
openBrowser: async (url: string) => {
41+
await open(url)
42+
}
43+
}
44+
let token
45+
try {
46+
const accounts = await pca.getAllAccounts()
47+
if (accounts.length > 0) {
48+
const response2 = await pca.acquireTokenSilent({ account: accounts[0], scopes: tokenRequest.scopes })
49+
token = response2.accessToken
50+
} else {
51+
const response = await pca.acquireTokenInteractive(tokenRequest)
52+
token = response.accessToken
53+
}
54+
} catch (error) {
55+
console.error('Error acquiring token interactively:', error)
56+
const response = await pca.acquireTokenInteractive(tokenRequest)
57+
token = response.accessToken
58+
}
59+
return token
60+
}
61+
62+
const createClient = async (): Promise<CopilotStudioClient> => {
63+
const settings = loadCopilotStudioConnectionSettingsFromEnv()
64+
const token = await acquireToken(settings)
65+
const copilotClient = new CopilotStudioClient(settings, token)
66+
console.log(`Copilot Studio Client Version: ${pkg.version}, running with settings: ${JSON.stringify(settings, null, 2)}`)
67+
return copilotClient
68+
}
69+
70+
const rl = readline.createInterface({
71+
input: process.stdin,
72+
output: process.stdout
73+
})
74+
75+
const askQuestion = async (copilotClient: CopilotStudioClient, conversationId: string) => {
76+
rl.question('\n>>>: ', async (answer) => {
77+
if (answer.toLowerCase() === 'exit') {
78+
rl.close()
79+
} else {
80+
const replies = await copilotClient.askQuestionAsync(answer, conversationId)
81+
replies.forEach((act: Activity) => {
82+
if (act.type === ActivityTypes.Message) {
83+
console.log(`\n${act.text}`)
84+
act.suggestedActions?.actions.forEach((action: CardAction) => console.log(action.value))
85+
} else if (act.type === ActivityTypes.EndOfConversation) {
86+
console.log(`\n${act.text}`)
87+
rl.close()
88+
}
89+
})
90+
await askQuestion(copilotClient, conversationId)
91+
}
92+
})
93+
}
94+
95+
const main = async () => {
96+
const copilotClient = await createClient()
97+
const act: Activity = await copilotClient.startConversationAsync(true)
98+
console.log('\nSuggested Actions: ')
99+
act.suggestedActions?.actions.forEach((action: CardAction) => console.log(action.value))
100+
await askQuestion(copilotClient, act.conversation?.id!)
101+
}
102+
103+
main().catch(e => console.log(e))
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
import fs from 'fs'
6+
import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'
7+
8+
export class MsalCachePlugin implements ICachePlugin {
9+
private cacheLocation: string = ''
10+
constructor(cacheLocation: string) {
11+
this.cacheLocation = cacheLocation
12+
}
13+
14+
async beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {
15+
return new Promise((resolve, reject) => {
16+
if (fs.existsSync(this.cacheLocation)) {
17+
fs.readFile(this.cacheLocation, 'utf-8', (error, data) => {
18+
if (error) {
19+
reject(error)
20+
} else {
21+
tokenCacheContext.tokenCache.deserialize(data)
22+
resolve()
23+
}
24+
})
25+
} else {
26+
fs.writeFile(this.cacheLocation, tokenCacheContext.tokenCache.serialize(), (error) => {
27+
if (error) {
28+
reject(error)
29+
}
30+
})
31+
}
32+
})
33+
}
34+
35+
async afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {
36+
return new Promise((resolve, reject) => {
37+
if (tokenCacheContext.cacheHasChanged) {
38+
fs.writeFile(this.cacheLocation, tokenCacheContext.tokenCache.serialize(), (error) => {
39+
if (error) {
40+
reject(error)
41+
}
42+
resolve()
43+
})
44+
} else {
45+
resolve()
46+
}
47+
})
48+
}
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"incremental": true,
4+
"lib": ["ES2021"],
5+
"target": "es2019",
6+
"declaration": true,
7+
"sourceMap": true,
8+
"composite": true,
9+
"strict": true,
10+
"moduleResolution": "node",
11+
"esModuleInterop": true,
12+
"skipLibCheck": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"resolveJsonModule": true,
15+
"module": "ESNext",
16+
"rootDir": "src",
17+
"outDir": "dist",
18+
"tsBuildInfoFile": "dist/.tsbuildinfo"
19+
}
20+
}

0 commit comments

Comments
 (0)