house-lights 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/skills/cast-list/assets/map.html +54 -32
  3. package/skills/cast-list/examples/agents/bodega-cupons/topics/OnError.topic.mcs.yml +2 -2
  4. package/skills/cast-list/examples/agents/bodega-cupons/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
  5. package/skills/cast-list/examples/agents/bodega-cupons/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +41 -0
  6. package/skills/cast-list/examples/agents/bodega-devolucoes/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
  7. package/skills/cast-list/examples/agents/bodega-devolucoes/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +56 -0
  8. package/skills/cast-list/examples/agents/bodega-frete/variables/PreferenciaAntiga.mcs.yml +9 -0
  9. package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/AvisoAntigo.topic.mcs.yml +12 -0
  10. package/skills/cast-list/examples/agents/bodega-loja-fisica/workflows/ListarLojas-3f2a9c1d/metadata.yml +3 -0
  11. package/skills/cast-list/examples/agents/bodega-loja-fisica/workflows/ListarLojas-3f2a9c1d/workflow.json +56 -0
  12. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteCupons.mcs.yml +1 -1
  13. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFidelidade.mcs.yml +24 -0
  14. package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFrete.mcs.yml +2 -14
  15. package/skills/cast-list/examples/agents/bodega-pedidos/topics/Escalate.topic.mcs.yml +5 -0
  16. package/skills/cast-list/examples/agents/bodega-pedidos/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
  17. package/skills/cast-list/examples/agents/bodega-pedidos/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +56 -0
  18. package/skills/cast-list/examples/equipe.csv +7 -0
  19. package/skills/cast-list/examples/inventario.csv +10 -0
  20. package/skills/cast-list/examples/telemetria.csv +10 -0
  21. package/skills/cast-list/references/format.md +35 -0
  22. package/skills/cast-list/references/keys.md +16 -0
  23. package/skills/cast-list/references/telemetry.md +39 -0
  24. package/skills/cast-list/scripts/build.mjs +90 -23
  25. package/skills/cast-list/scripts/engine.mjs +111 -108
@@ -0,0 +1,24 @@
1
+ mcs.metadata:
2
+ componentName: Agente Fidelidade
3
+ description: Agente conectado Bodega Azul - Fidelidade.
4
+ kind: TaskDialog
5
+ inputs:
6
+ - kind: ManualTaskInput
7
+ propertyName: CategoriaSolicitacao
8
+ value: =Global.CategoriaSolicitacao
9
+
10
+ modelDisplayName: Bodega Azul - Fidelidade
11
+ modelDescription: "Agente especialista no programa de fidelidade: pontos, níveis e resgates. Use quando a categoria for Fidelidade."
12
+ action:
13
+ kind: InvokeConnectedAgentTaskAction
14
+ inputType:
15
+ properties:
16
+ CategoriaSolicitacao:
17
+ displayName: CategoriaSolicitacao
18
+ description: Categoria da solicitação definida pelo orquestrador.
19
+ isRequired: true
20
+ type: String
21
+
22
+ botSchemaName: cr7b2_bodegaFidelidade
23
+ historyType:
24
+ kind: ConversationHistory
@@ -2,23 +2,11 @@ mcs.metadata:
2
2
  componentName: Agente Frete
3
3
  description: Agente conectado Bodega Azul - Frete.
4
4
  kind: TaskDialog
5
- inputs:
6
- - kind: ManualTaskInput
7
- propertyName: CategoriaSolicitacao
8
- value: =Global.CategoriaSolicitacao
9
-
10
5
  modelDisplayName: Bodega Azul - Frete
11
6
  modelDescription: "Agente especialista em frete e entregas: modalidades, prazos por região e frete grátis. Use quando a categoria for Frete."
12
7
  action:
13
8
  kind: InvokeConnectedAgentTaskAction
14
- inputType:
15
- properties:
16
- CategoriaSolicitacao:
17
- displayName: CategoriaSolicitacao
18
- description: Categoria da solicitação definida pelo orquestrador.
19
- isRequired: true
20
- type: String
21
-
9
+ inputType: {}
22
10
  botSchemaName: cr7b2_bodegaFrete
23
11
  historyType:
24
- kind: ConversationHistory
12
+ kind: NoHistory
@@ -18,3 +18,8 @@ beginDialog:
18
18
  - kind: SendActivity
19
19
  id: sendMessage_X7qDFG
20
20
  activity: "A transferência para um atendente ainda não está disponível neste canal. Fale com a nossa equipe pelo e-mail pedidos@bodegaazul.example, de segunda a sábado, das 9h às 18h."
21
+
22
+ - kind: SendActivity
23
+ id: sendMessage_kDOncS
24
+ disabled: true
25
+ activity: "Aguarde, estamos transferindo você para um atendente."
@@ -0,0 +1,3 @@
1
+ displayName: Abrir chamado no ERP
2
+ workflowId: 7d1c2a4e-3b5f-4c6d-8e9f-0a1b2c3d4e5f
3
+ jsonFileName: workflow.json
@@ -0,0 +1,56 @@
1
+ {
2
+ "properties": {
3
+ "displayName": "Abrir chamado no ERP",
4
+ "connectionReferences": {},
5
+ "definition": {
6
+ "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
7
+ "contentVersion": "1.0.0.0",
8
+ "triggers": {
9
+ "manual": {
10
+ "type": "Request",
11
+ "kind": "Skills",
12
+ "inputs": {
13
+ "schema": {
14
+ "type": "object",
15
+ "properties": {
16
+ "resumo": {
17
+ "type": "string"
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ },
24
+ "actions": {
25
+ "Abrir_chamado": {
26
+ "type": "Scope",
27
+ "actions": {
28
+ "Chamar_ERP": {
29
+ "type": "Http",
30
+ "inputs": {
31
+ "method": "POST",
32
+ "uri": "https://erp.bodega-azul.example/chamados",
33
+ "body": "@triggerBody()"
34
+ }
35
+ }
36
+ }
37
+ },
38
+ "Responder": {
39
+ "type": "Response",
40
+ "kind": "Skills",
41
+ "runAfter": {
42
+ "Abrir_chamado": [
43
+ "Succeeded"
44
+ ]
45
+ },
46
+ "inputs": {
47
+ "statusCode": 200,
48
+ "body": {
49
+ "resultado": "@outputs('Resultado')"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ Agente;KPI;Dono
2
+ Bodega Azul - Pedidos;pedido localizado em até 2 trocas de mensagem;Squad Pedidos
3
+ Bodega Azul - Devoluções;troca aberta sem atendente em 80% dos casos;Squad Pós-venda
4
+ Bodega Azul - Frete;cotação em menos de 5 s;Squad Logística
5
+ Bodega Azul - Cupons;cupom validado na primeira tentativa;Squad Promoções
6
+ Bodega Azul - Loja Física;;Squad Lojas
7
+ Sobre a Bodega Azul;respostas gerais sem encaminhar;Squad Atendimento
@@ -0,0 +1,10 @@
1
+ Item name,Item type,Schema name,Owner,Environment,Created on,Last published
2
+ Bodega Azul - Orquestrador,Agent,cr7b2_bodegaOrquestrador,atendimento@bodegaazul.example,Bodega Azul (Produção),2026-03-02,2026-10-28
3
+ Bodega Azul - Pedidos,Agent,cr7b2_bodegaPedidos,pedidos@bodegaazul.example,Bodega Azul (Produção),2026-03-05,2026-10-21
4
+ Bodega Azul - Devoluções,Agent,cr7b2_bodegaDevolucoes,trocas@bodegaazul.example,Bodega Azul (Produção),2026-03-05,2026-10-14
5
+ Bodega Azul - Frete,Agent,cr7b2_bodegaFrete,frete@bodegaazul.example,Bodega Azul (Produção),2026-03-09,2026-09-30
6
+ Bodega Azul - Cupons,Agent,cr7b2_bodegaCupons,,Bodega Azul (Produção),2026-03-12,2026-10-02
7
+ Bodega Azul - Loja Física,Agent,cr7b2_bodegaLojaFisica,loja@bodegaazul.example,Bodega Azul (Produção),2026-04-01,
8
+ Bodega Azul - Fidelidade,Agent,cr7b2_bodegaFidelidade,fidelidade@bodegaazul.example,Bodega Azul (Testes),2026-08-20,
9
+ Painel de Pedidos,Canvas app,,pedidos@bodegaazul.example,Bodega Azul (Produção),2026-02-11,
10
+ Abrir chamado no ERP,Agent flow,,pedidos@bodegaazul.example,Bodega Azul (Produção),2026-03-05,
@@ -0,0 +1,10 @@
1
+ agente,evento,vezes
2
+ Bodega Azul - Orquestrador,TopicStart,1840
3
+ Bodega Azul - Orquestrador,BodegaOrquestrador_OnError,12
4
+ Bodega Azul - Pedidos,TopicStart,610
5
+ Bodega Azul - Pedidos,BodegaPedidos_OnError,7
6
+ Bodega Azul - Devoluções,TopicStart,402
7
+ Bodega Azul - Devoluções,BodegaDevolucoes_OnError,3
8
+ Bodega Azul - Frete,TopicStart,355
9
+ Bodega Azul - Cupons,TopicStart,290
10
+ Bodega Azul - Cupons,bodega_cupons_onerror,5
@@ -35,6 +35,15 @@ decisions: # what the team decided about a pattern
35
35
 
36
36
  ## The workbook back
37
37
 
38
+ ## A spreadsheet from outside
39
+
40
+ `--declared <table.csv|.xlsx>` (more than once is fine) reads a table the team already has — the admin center's
41
+ inventory download, the team's list — and proposes a `declared:` line per agent and column, with the source
42
+ `<file> › line <n>` (`<file> › <sheet> › line <n>` for an .xlsx). The agent's column is found by its values; the key is
43
+ the column's title in lower case with `-` (`Linha de base` → `linha-de-base`; `Dono`/`Owner` → `owner`, `KPI`/`Indicador`
44
+ → `kpi`). Listed with keys (`d1`…), written with `--take`, like the workbook below. Two tables saying the same key of
45
+ the same agent: the later one holds.
46
+
38
47
  In Excel, fill the **Declared** sheet (who, key, value, source) and the **Decision** column of **Patterns** (`accepted` /
39
48
  `aceito`, `rejected` / `rejeitado`; **Scope** and **Why** beside it). Then:
40
49
 
@@ -55,3 +64,29 @@ A line deleted or a decision cleared is a change too.
55
64
  "facts": { "trigger:OnUnknownIntent": [ { "file": "topics/Fallback.topic.mcs.yml", "line": 7 } ] },
56
65
  "children": [], "unknown": [] } ] }
57
66
  ```
67
+
68
+ An orchestrator's agent also has `contract` — a line per connected-agent action: `calls` (the schema name it calls),
69
+ `name` (the action's name for it), `history` (it passes the conversation history), `inputs`, `description` (the
70
+ routing description), `file` and `line` — and every agent has `flows`: `id` (the `workflowId`), `name`, `folder`,
71
+ `uses` (`http:<host>`, `connector:<api>`, `operation:<api>/<op>`, `child-flow`), `returns`, `called` and `file`.
72
+
73
+ ## The facts of the contract, the flows and what nobody uses
74
+
75
+ | Key | On which agent | From |
76
+ |---|---|---|
77
+ | `called-with:history` | the one called | the orchestrator's action has `historyType: ConversationHistory` |
78
+ | `called-with:input:<name>` | the one called | each input the orchestrator's action passes |
79
+ | `routing:described` | the one called | the action has a `modelDescription` |
80
+ | `flow-uses:http:<host>`, `flow-uses:connector:<api>`, `flow-uses:operation:<api>/<op>`, `flow-uses:child-flow` | the one whose node calls the flow | the actions of `workflow.json`, at any depth |
81
+ | `flow-returns` | the same | the flow has a `Response` action |
82
+ | `orphan:topic:<name>`, `orphan:action:<name>`, `orphan:variable:<name>`, `orphan:flow:<name>` | the one that has it | nothing can reach it (see SKILL.md, step 2) |
83
+ | `disabled:node:<kind>`, `disabled:<topic\|action\|variable\|knowledge>:<name>` | the one that has it | `disabled: true` on a node; `state` or `status: Inactive` on a component |
84
+
85
+ | `telemetry:event:<form>` | the one that logs it | a `LogCustomTelemetryEvent` node; the form is its `eventName` with the agent's own name as `{agent}` (part of `node:LogCustomTelemetryEvent`) |
86
+ | `telemetry:field:<form>/<field>` | the same | each field of the node's `properties` record (part of its event) |
87
+
88
+ Every agent also has `telemetry` in `<id>.facts.json`: `name` (as written), `form`, `fields` (absent when the record is
89
+ a formula), `file` and `line`. An event spelt another way counts as the most common form in the comparison.
90
+
91
+ The contract's facts cite the orchestrator's file (`<orchestrator>/actions/X.mcs.yml`). `orphan:` and `disabled:` are
92
+ never a pattern nor a solo, and what an unused flow calls is not a fact.
@@ -30,6 +30,11 @@ Every fact cast-list finds is a key. The keys below have a name; any other key k
30
30
 
31
31
  | Key | Name | Nome |
32
32
  |---|---|---|
33
+ | `flow-returns` | The flow returns an answer | O fluxo devolve uma resposta |
34
+ | `flow-uses:child-flow` | The flow calls another flow | O fluxo chama outro fluxo |
35
+ | `flow-uses:http:<value>` | Flow calls <value> | Fluxo chama <valor> |
36
+ | `flow-uses:connector:<value>` | Flow uses the connector <value> | Fluxo usa o conector <valor> |
37
+ | `flow-uses:operation:<value>` | Flow uses the operation <value> | Fluxo usa a operação <valor> |
33
38
  | `connector:<value>` | Connector <value> | Conector <valor> |
34
39
  | `operation:<value>` | Operation <value> | Operação <valor> |
35
40
  | `connref:<value>` | Connection reference <value> | Referência de conexão <valor> |
@@ -58,6 +63,9 @@ Every fact cast-list finds is a key. The keys below have a name; any other key k
58
63
  | `trigger:OnRedirect` | Takes a call from the orchestrator | Recebe chamada do orquestrador |
59
64
  | `trigger:OnToolSelected` | Child chosen as a tool | Filho escolhido como ferramenta |
60
65
  | `setting:connectable` | Can be called by an orchestrator | Pode ser chamado por um orquestrador |
66
+ | `called-with:history` | Receives the conversation history | Recebe o histórico da conversa |
67
+ | `routing:described` | Has a routing description | Tem descrição de roteamento |
68
+ | `called-with:input:<value>` | Receives <value> from the orchestrator <value> | Recebe <valor> do orquestrador <valor> |
61
69
  | `calls:<value>` | Calls <value> | Chama <valor> |
62
70
  | `child:<value>` | Child <value> | Filho <valor> |
63
71
  | `var-in:<value>` | Receives from the orchestrator <value> | Recebe do orquestrador <valor> |
@@ -72,6 +80,14 @@ Every fact cast-list finds is a key. The keys below have a name; any other key k
72
80
  | `instructions:present` | Has instructions | Tem instruções |
73
81
  | `node:LogCustomTelemetryEvent` | Logs telemetry | Registra telemetria |
74
82
  | `node:CSATQuestion` | Asks for satisfaction (CSAT) | Pergunta a satisfação (CSAT) |
83
+ | `orphan:topic:<value>` | Topic nobody calls <value> | Tópico que ninguém chama <valor> |
84
+ | `orphan:action:<value>` | Tool nobody can choose <value> | Ferramenta que ninguém escolhe <valor> |
85
+ | `orphan:variable:<value>` | Variable nobody uses <value> | Variável que ninguém usa <valor> |
86
+ | `orphan:flow:<value>` | Flow nobody calls <value> | Fluxo que ninguém chama <valor> |
87
+ | `disabled:node:<value>` | Switched-off node <value> | Nó desligado <valor> |
88
+ | `telemetry:event:<value>` | Logs the event <value> | Registra o evento <valor> |
89
+ | `telemetry:field:<value>` | The event carries the field (<event>/<field>) <value> | O evento leva o campo (<evento>/<campo>) <valor> |
90
+ | `disabled:<value>` | Switched off <value> | Desligado <valor> |
75
91
  | `node:<value>` | Node <value> | Nó <valor> |
76
92
  | `model:<value>` | Model <value> | Modelo <valor> |
77
93
  | `auth:<value>` | Sign-in <value> | Login <valor> |
@@ -0,0 +1,39 @@
1
+ # What fired: the export of Application Insights
2
+
3
+ The YAML shows what an agent **should** log (`LogCustomTelemetryEvent`). What it **does** log is in Application
4
+ Insights, when the agent is connected to it (Copilot Studio › Settings › Advanced › Application Insights). cast-list
5
+ never reads it over the network: the person runs one query and exports the result, and the analysis reads the file.
6
+
7
+ ## The query
8
+
9
+ In the Application Insights resource, open **Logs**, paste this, and export the result (**Export › CSV**):
10
+
11
+ ```kusto
12
+ customEvents
13
+ | where timestamp > ago(30d)
14
+ | where tostring(customDimensions['designMode']) == "False"
15
+ | summarize vezes = count() by agente = cloud_RoleInstance, evento = name
16
+ ```
17
+
18
+ - `designMode` leaves out the conversations of the test panel.
19
+ - `cloud_RoleInstance` is the agent's name in the default telemetry. When the team puts the agent's name in a field of
20
+ its own events (such as `Agente`), use that instead: `agente = tostring(customDimensions['Agente'])`. cast-list finds
21
+ the agent's column by its values (the schema name, the folder or the display name), not by its title.
22
+ - Change `30d` to the period you want, and pass the same number with `--telemetry-days`.
23
+ - **Export only these three columns.** Never add `text`, `fromName`, `user_Id` or any other column with what people
24
+ said or who they are: cast-list reads only the agent, the event and the times, and the rest has no place in the file.
25
+
26
+ ## Run
27
+
28
+ ```
29
+ node scripts/build.mjs <agents-folder> --all --telemetry eventos.csv [--telemetry-days 30] [--lang pt]
30
+ ```
31
+
32
+ The file is read on this run only (it is not kept in the cast file): pass it again on the next one.
33
+
34
+ - «O evento «X» de y não disparou em 30 dias» — the event is in y's YAML, y has lines in the export, and none is X.
35
+ - «y não tem nenhuma linha na telemetria: está ligado ao Application Insights?» — y is not in the export at all.
36
+ - An event of the export that no YAML has — Copilot Studio's own (`TopicStart`, `BotMessageReceived`…) — is left out.
37
+
38
+ The map's Cast tab shows each agent's events with the times they fired, and the workbook's **Telemetry** sheet has a
39
+ column with the times.
@@ -9,6 +9,11 @@
9
9
  // node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]
10
10
  // what was filled in the workbook (Declared, the Decision column): listed with a key each,
11
11
  // and with --take written to the cast file
12
+ // node build.mjs <agents-folder> --declared <table.csv|.xlsx> [--declared <another>] [--agent-column <title>] [--take <key,…|all>]
13
+ // what a spreadsheet says about the agents (the team's, the admin center's inventory): each
14
+ // column becomes a declared fact, listed with a key, and with --take written to the cast file
15
+ // … --telemetry <export.csv|.xlsx> [--telemetry-days <n>] what fired, from Application Insights (references/telemetry.md):
16
+ // the events in the YAML that did not fire, and who has no line at all
12
17
  //
13
18
  // With no --html/--xlsx/--lesson it makes the map. Files go in --out (or beside the folder), named by the folder:
14
19
  // <id>.html, <id>.xlsx, <id>.lesson.html (a chalk-talk lesson), and <id>.cast.yaml, kept up to date (read again on every
@@ -20,23 +25,26 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
20
25
  import { basename, dirname, join, resolve } from 'node:path'
21
26
  import { fileURLToPath } from 'node:url'
22
27
  import { runPage } from './browser.mjs'
23
- import { agentsIn, applyChanges, compare, DEFAULT_PROFILE, extractAgent, fromSheets, knownKinds, labelOf, lessonOf, problemText, readCast, readProfile, reviewCast, validateScene, workbookFile, workbookOf, writeCast } from './engine.mjs'
28
+ import { agentsIn, applyChanges, compare, declaredFrom, DEFAULT_PROFILE, extractAgent, firedFrom, fromSheets, mergeDeclared, knownKinds, labelOf, lessonOf, problemText, readCast, readCsv, readProfile, reviewCast, validateScene, workbookFile, workbookOf, writeCast } from './engine.mjs'
24
29
  import { readXlsx } from './read-xlsx.mjs'
25
30
  import { fill, inline } from './page.mjs'
26
31
 
27
32
  const here = dirname(fileURLToPath(import.meta.url))
28
33
  const argv = process.argv.slice(2)
29
- const valued = new Set(['--lang', '--out', '--profile', '--from', '--take'])
34
+ const valued = new Set(['--lang', '--out', '--profile', '--from', '--take', '--declared', '--agent-column', '--telemetry', '--telemetry-days'])
30
35
  const flags = new Set(['--check', '--html', '--xlsx', '--lesson', '--all', '--facts', ...valued])
31
36
  const option = (name) => {
32
37
  const at = argv.indexOf(name)
33
38
  return at >= 0 ? argv[at + 1] : undefined
34
39
  }
40
+ const options = (name) => argv.flatMap((a, i) => (a === name && argv[i + 1] ? [argv[i + 1]] : []))
35
41
  const args = argv.filter((a, i) => !a.startsWith('--') && !valued.has(argv[i - 1]))
36
42
  const usage = `usage: node build.mjs <agents-folder> --check [--profile <p.cast.yaml>] [--lang en|pt]
37
43
  node build.mjs <agents-folder> [--profile <p.cast.yaml>] [--html] [--xlsx] [--lesson] [--all] [--lang en|pt] [--out <folder>]
38
44
  node build.mjs <agents-folder> --facts [--out <folder>]
39
- node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]`
45
+ node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]
46
+ node build.mjs <agents-folder> --declared <table.csv|.xlsx> [--agent-column <title>] [--take <key,key…|all>] [--out <folder>]
47
+ … [--telemetry <export.csv|.xlsx>] [--telemetry-days <n>]`
40
48
  const lang = option('--lang') ?? 'en'
41
49
  const fail = (msg) => {
42
50
  console.error(`${msg}\n${usage}`)
@@ -47,7 +55,11 @@ const unknown = argv.find((a) => a.startsWith('--') && !flags.has(a))
47
55
  if (unknown) fail(`✗ unknown option ${unknown}`)
48
56
  if (!['en', 'pt'].includes(lang)) fail('✗ --lang is en or pt')
49
57
  for (const v of valued) if (argv.includes(v) && !option(v)) fail(`✗ ${v} needs a value`)
50
- if (argv.includes('--take') && !argv.includes('--from')) fail('✗ --take goes with --from')
58
+ if (argv.includes('--take') && !argv.includes('--from') && !argv.includes('--declared')) fail('✗ --take goes with --from or --declared')
59
+ if (argv.includes('--from') && argv.includes('--declared')) fail('✗ --from or --declared, one at a time')
60
+ for (const f of [...options('--declared'), ...options('--telemetry')]) if (!existsSync(f) || !/\.(csv|xlsx)$/i.test(f)) fail(`✗ ${f}: a .csv or an .xlsx that is there`)
61
+ const days = Number(option('--telemetry-days') ?? 30)
62
+ if (!Number.isInteger(days) || days < 1) fail('✗ --telemetry-days is a whole number of days')
51
63
 
52
64
  const T = {
53
65
  en: {
@@ -57,6 +69,12 @@ const T = {
57
69
  changes: (n) => (n ? `${n} change${n === 1 ? '' : 's'} in the workbook (take them with --take <key,…> or --take all):` : '✓ the workbook changes nothing'),
58
70
  took: (n, f) => `✓ took ${n} change${n === 1 ? '' : 's'} into ${f}`, unknownKey: (k) => `✗ no change with the key «${k}»`,
59
71
  declared: 'declared', decision: 'decision', add: '+', set: '~', remove: '−', accepted: 'accepted', rejected: 'rejected', platform: 'platform', specialist: 'specialist', orchestrator: 'orchestrator', none: (f) => `✗ ${f} has no folder with agent.mcs.yml`, privacy: '• the files hold the client’s data: keep them in the client’s environment',
72
+ noColumn: (f, cols) => `✗ ${f}: no column names the agents of the folder (by schema name, folder or display name): say which with --agent-column "<title>" (columns: ${cols.join(', ')})`,
73
+ tie: (f, cols) => `✗ ${f}: the columns ${cols.map((c) => `«${c}»`).join(' and ')} name the agents in different ways: say which with --agent-column "<title>"`,
74
+ noHeader: (f, cols) => `✗ ${f}: no column with that title (columns: ${cols.join(', ')})`,
75
+ noEvent: (f, cols) => `✗ ${f}: no column of the event («evento», «event» or «name»; columns: ${cols.join(', ')})`,
76
+ unmatched: (n, f, list) => `• ${f}: ${n} ${n === 1 ? 'row names' : 'rows name'} nobody in the folder: ${list}`, line: 'line',
77
+ telemetry: (f, n, d) => `✓ telemetry ${f}: ${n} ${n === 1 ? 'row' : 'rows'} of the folder's agents, ${d} days`,
60
78
  },
61
79
  pt: {
62
80
  found: (n, o, s) => `✓ ${n} no elenco: ${o} ${o === 1 ? 'orquestrador' : 'orquestradores'}, ${s} ${s === 1 ? 'especialista' : 'especialistas'}`,
@@ -65,6 +83,12 @@ const T = {
65
83
  changes: (n) => (n ? `${n} ${n === 1 ? 'mudança' : 'mudanças'} na planilha (aceite com --take <chave,…> ou --take all):` : '✓ a planilha não muda nada'),
66
84
  took: (n, f) => `✓ ${n} ${n === 1 ? 'mudança gravada' : 'mudanças gravadas'} em ${f}`, unknownKey: (k) => `✗ nenhuma mudança com a chave «${k}»`,
67
85
  declared: 'declarado', decision: 'decisão', add: '+', set: '~', remove: '−', accepted: 'aceito', rejected: 'rejeitado', platform: 'plataforma', specialist: 'especialista', orchestrator: 'orquestrador', none: (f) => `✗ ${f} não tem nenhuma pasta com agent.mcs.yml`, privacy: '• os arquivos têm dados do cliente: deixe-os no ambiente dele',
86
+ noColumn: (f, cols) => `✗ ${f}: nenhuma coluna cita os agentes da pasta (pelo nome de esquema, pela pasta ou pelo nome de exibição): diga qual com --agent-column "<título>" (colunas: ${cols.join(', ')})`,
87
+ tie: (f, cols) => `✗ ${f}: as colunas ${cols.map((c) => `«${c}»`).join(' e ')} citam os agentes de jeitos diferentes: diga qual com --agent-column "<título>"`,
88
+ noHeader: (f, cols) => `✗ ${f}: nenhuma coluna com esse título (colunas: ${cols.join(', ')})`,
89
+ noEvent: (f, cols) => `✗ ${f}: nenhuma coluna do evento («evento», «event» ou «name»; colunas: ${cols.join(', ')})`,
90
+ unmatched: (n, f, list) => `• ${f}: ${n} ${n === 1 ? 'linha não cita' : 'linhas não citam'} ninguém da pasta: ${list}`, line: 'linha',
91
+ telemetry: (f, n, d) => `✓ telemetria ${f}: ${n} ${n === 1 ? 'linha' : 'linhas'} dos agentes da pasta, ${d} dias`,
68
92
  },
69
93
  }[lang]
70
94
 
@@ -84,36 +108,45 @@ const castFile = join(out, `${id}.cast.yaml`)
84
108
  const castText = existsSync(castFile) ? readFileSync(castFile, 'utf8') : undefined
85
109
  const cast = castText !== undefined ? read(castFile, readCast).cast : { ...(profile.name ? { name: profile.name } : {}), agents: [], blocks: [], declared: [], decisions: [] }
86
110
 
87
- // ---- the way back: an edited workbook as the changes it proposes ----
88
- if (argv.includes('--from')) {
89
- const edited = fromSheets(cast, readXlsx(readFileSync(resolve(option('--from')))), Object.fromEntries(profile.types.map((t) => [t.id, t.name])))
90
- if (edited.problems.length) {
91
- for (const p of edited.problems) console.error(`✗ ${basename(option('--from'))}: ${p}`)
92
- process.exit(1)
93
- }
94
- const line = (c) => {
95
- const x = c.after ?? c.before
96
- // a key with no kind («kpi») is something only people know: declared:<key>
97
- if (c.kind === 'declared') return `${T[c.op]} ${T.declared} ${x.agent} · ${labelOf(x.key.includes(':') ? x.key : `declared:${x.key}`, lang)}${x.value ? ` = «${x.value}»` : ''}${x.source ? ` (${x.source})` : ''}`
98
- return `${T[c.op]} ${T.decision} ${labelOf(x.key, lang)}: ${T[x.status]} (${T[x.scope]})${x.why ? ` — ${x.why}` : ''}`
99
- }
111
+ // a table from outside: the sheets of an .xlsx, or a .csv
112
+ const tablesOf = (file) => (/\.xlsx$/i.test(file) ? readXlsx(readFileSync(file)) : [readCsv(readFileSync(file, 'utf8'), '')])
113
+ const tableProblem = (file, p) => {
114
+ console.error(p.problem === 'tie' ? T.tie(basename(file), p.titles) : p.problem === 'header' ? T.noHeader(basename(file), p.titles) : p.problem === 'event' ? T.noEvent(basename(file), p.titles) : T.noColumn(basename(file), p.titles))
115
+ process.exit(1)
116
+ }
117
+ const line = (c) => {
118
+ const x = c.after ?? c.before
119
+ if (c.kind === 'declared') return `${T[c.op]} ${T.declared} ${x.agent} · ${labelOf(x.key.includes(':') ? x.key : `declared:${x.key}`, lang)}${x.value ? ` = «${x.value}»` : ''}${x.source ? ` (${x.source})` : ''}`
120
+ return `${T[c.op]} ${T.decision} ${labelOf(x.key, lang)}: ${T[x.status]} (${T[x.scope]})${x.why ? ` — ${x.why}` : ''}`
121
+ }
122
+ // what changes the cast file: listed, each with its key, and with --take written
123
+ const proposeOrTake = (changes) => {
100
124
  const take = option('--take')
101
125
  if (!take) {
102
- console.log(T.changes(edited.changes.length))
103
- for (const c of edited.changes) console.log(` ${c.key} ${line(c)}`)
126
+ console.log(T.changes(changes.length))
127
+ for (const c of changes) console.log(` ${c.key} ${line(c)}`)
104
128
  process.exit(0)
105
129
  }
106
- const keys = take === 'all' ? edited.changes.map((c) => c.key) : take.split(',').map((k) => k.trim()).filter(Boolean)
107
- const missing = keys.find((k) => !edited.changes.some((c) => c.key === k))
130
+ const keys = take === 'all' ? changes.map((c) => c.key) : take.split(',').map((k) => k.trim()).filter(Boolean)
131
+ const missing = keys.find((k) => !changes.some((c) => c.key === k))
108
132
  if (missing) {
109
133
  console.error(T.unknownKey(missing))
110
134
  process.exit(2)
111
135
  }
112
136
  mkdirSync(out, { recursive: true })
113
- writeFileSync(castFile, writeCast(applyChanges(cast, edited.changes, keys), castText))
137
+ writeFileSync(castFile, writeCast(applyChanges(cast, changes, keys), castText))
114
138
  console.log(T.took(keys.length, basename(castFile)))
115
139
  process.exit(0)
116
140
  }
141
+ // ---- the way back: an edited workbook as the changes it proposes ----
142
+ if (argv.includes('--from')) {
143
+ const edited = fromSheets(cast, readXlsx(readFileSync(resolve(option('--from')))), Object.fromEntries(profile.types.map((t) => [t.id, t.name])))
144
+ if (edited.problems.length) {
145
+ for (const p of edited.problems) console.error(`✗ ${basename(option('--from'))}: ${p}`)
146
+ process.exit(1)
147
+ }
148
+ proposeOrTake(edited.changes)
149
+ }
117
150
 
118
151
  // the schema's kinds, to tell a made-up one; without the schema every kind is taken as real
119
152
  const schema = join(here, '../vendor/microsoft/reference/bot.schema.yaml-authoring.json')
@@ -132,7 +165,41 @@ if (argv.includes('--facts')) {
132
165
  console.log(T.privacy)
133
166
  process.exit(0)
134
167
  }
135
- const result = compare(found, profile, cast)
168
+ if (argv.includes('--declared')) {
169
+ const changes = []
170
+ for (const file of options('--declared')) {
171
+ const sheets = tablesOf(file)
172
+ let read = 0
173
+ let problem
174
+ for (const t of sheets) {
175
+ const r = declaredFrom(t, found, cast, { file: basename(file), lang, agentColumn: option('--agent-column') })
176
+ if (r.problem) {
177
+ problem ??= r.problem
178
+ continue
179
+ }
180
+ read++
181
+ changes.push(...r.changes)
182
+ if (r.unmatched.length) console.log(T.unmatched(r.unmatched.length, basename(file), `${r.unmatched.slice(0, 5).map((u) => `${T.line} ${u.line} («${u.value}»)`).join(', ')}${r.unmatched.length > 5 ? ', …' : ''}`))
183
+ }
184
+ if (!read) tableProblem(file, problem)
185
+ }
186
+ proposeOrTake(mergeDeclared(changes))
187
+ }
188
+ let fired
189
+ if (option('--telemetry')) {
190
+ const file = option('--telemetry')
191
+ let problem
192
+ for (const t of tablesOf(file)) {
193
+ const r = firedFrom(t, found, { file: basename(file), days, agentColumn: option('--agent-column') })
194
+ if ('problem' in r) problem ??= r.problem
195
+ else {
196
+ fired = r
197
+ break
198
+ }
199
+ }
200
+ if (!fired) tableProblem(file, problem)
201
+ }
202
+ const result = compare(found, profile, cast, fired)
136
203
  const looks = reviewCast(result)
137
204
  const roles = Object.values(result.roles)
138
205
  console.log(T.found(found.length, roles.filter((r) => r === 'orchestrator').length, roles.filter((r) => r === 'specialist').length))