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.
- package/package.json +1 -1
- package/skills/cast-list/assets/map.html +54 -32
- package/skills/cast-list/examples/agents/bodega-cupons/topics/OnError.topic.mcs.yml +2 -2
- package/skills/cast-list/examples/agents/bodega-cupons/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
- package/skills/cast-list/examples/agents/bodega-cupons/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +41 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
- package/skills/cast-list/examples/agents/bodega-devolucoes/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +56 -0
- package/skills/cast-list/examples/agents/bodega-frete/variables/PreferenciaAntiga.mcs.yml +9 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/topics/AvisoAntigo.topic.mcs.yml +12 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/workflows/ListarLojas-3f2a9c1d/metadata.yml +3 -0
- package/skills/cast-list/examples/agents/bodega-loja-fisica/workflows/ListarLojas-3f2a9c1d/workflow.json +56 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteCupons.mcs.yml +1 -1
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFidelidade.mcs.yml +24 -0
- package/skills/cast-list/examples/agents/bodega-orquestrador/actions/AgenteFrete.mcs.yml +2 -14
- package/skills/cast-list/examples/agents/bodega-pedidos/topics/Escalate.topic.mcs.yml +5 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/workflows/AbrirChamadoERP-7d1c2a4e/metadata.yml +3 -0
- package/skills/cast-list/examples/agents/bodega-pedidos/workflows/AbrirChamadoERP-7d1c2a4e/workflow.json +56 -0
- package/skills/cast-list/examples/equipe.csv +7 -0
- package/skills/cast-list/examples/inventario.csv +10 -0
- package/skills/cast-list/examples/telemetria.csv +10 -0
- package/skills/cast-list/references/format.md +35 -0
- package/skills/cast-list/references/keys.md +16 -0
- package/skills/cast-list/references/telemetry.md +39 -0
- package/skills/cast-list/scripts/build.mjs +90 -23
- 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:
|
|
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,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
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
103
|
-
for (const c of
|
|
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' ?
|
|
107
|
-
const missing = keys.find((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,
|
|
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
|
-
|
|
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))
|