| name | importar-respostas-excel |
| description | Converts an Excel exported from Microsoft Forms (or Google Forms / multi-respondent spreadsheet) into structured respostas.json for the AI Maturity Assessment, aggregating multiple respondents via mean per question. Use when the client collected responses via Forms and wants to run the pipeline. Trigger on "importar respostas", "import Forms", "converter Excel para JSON", "respostas-forms.xlsx", "Microsoft Forms para o assessment", "agregar respondentes". Looks for respostas-forms.xlsx at workspace root or path passed by the user. |
Skill: Import responses from Excel (Microsoft Forms)
When to use
- Client created a Microsoft Forms with the 158 questions and wants to import responses.
- Client has multiple respondents (3+) and wants automatic mean aggregation.
- Before running
/pipeline-completo or /calcular-scores when input is Excel.
Inputs
- Excel:
respostas-forms.xlsx at workspace root (default) or path passed as argument.
framework.json: to map column â qid and validate all 158 questions are present.
coleta/perguntas-para-forms.md: reference if there's doubt about header format.
Expected output
respostas.json overwritten (with automatic backup at respostas.json.backup-<timestamp>).
saida/import-log-<DATE>.md: import log (how many respondents, how many questions, conflicts resolved, alerts).
Expected Excel format (Microsoft Forms export)
| A | B | C | D | E | F | G | H |
| ID | Start time | Completion time | Email | Name | P1-C1-Q1: <question> | EvidĂȘncia (P1-C1-Q1) | P1-C1-Q2: <question> |
| 1 | timestamp | timestamp | x@y.com | JoĂŁo | L3 â Gerenciado â ... | "evidence text" | L2 â Definido â ... |
| 2 | ... | ... | ... | Ana | L4 â Otimizando â ... | "text" | (empty = didn't answer) |
- Row 1 = headers
- Rows 2+ = one respondent per row
- Columns F+ alternate: question (Choice) â evidence (Long Text) â next question...
- Question header ALWAYS starts with
qid in pattern P[1-3]-C[1-9][0-9]?-Q[1-9][0-9]?:
Procedure
1. Locate and validate Excel
path = user_argument or "respostas-forms.xlsx"
if not exists â error: "NĂŁo encontrei o arquivo. Verifique o caminho ou rode com /importar-respostas-excel <caminho>"
2. Extract column â qid mapping
import re, openpyxl
wb = openpyxl.load_workbook(path)
ws = wb.active
qid_pattern = re.compile(r"^(P[1-3]-C\d+-Q\d+):")
col_to_qid = {}
col_to_evidence_qid = {}
for col_idx, header_cell in enumerate(ws[1], start=1):
val = str(header_cell.value or "").strip()
m = qid_pattern.match(val)
if m:
col_to_qid[col_idx] = m.group(1)
elif val.startswith("EvidĂȘncia ("):
em = re.match(r"EvidĂȘncia \(([^)]+)\)", val)
if em:
col_to_evidence_qid[col_idx] = em.group(1)
Validate: len(col_to_qid) should be close to 158. If < 100, alert and stop.
3. Map option â level
def parse_level(cell_value):
"""Forms exports the FULL option. E.g.: 'L3 â Gerenciado â >75% com mĂ©tricas'.
Take first 2 chars."""
if not cell_value:
return None
s = str(cell_value).strip()
if s.startswith("L0"): return 0
if s.startswith("L1"): return 1
if s.startswith("L2"): return 2
if s.startswith("L3"): return 3
if s.startswith("L4"): return 4
if s.startswith("NA") or s.lower() in ("nĂŁo sei", "n/a", "na"):
return None
return None
4. Collect responses per respondent
respondents = []
for row in ws.iter_rows(min_row=2, values_only=False):
name = row[4].value if len(row) > 4 else ""
email = row[3].value if len(row) > 3 else ""
if not name and not email:
continue
r = {"name": name, "email": email, "responses": {}}
for col_idx, qid in col_to_qid.items():
cell = row[col_idx - 1]
level = parse_level(cell.value)
if level is None and cell.value:
log_warning(f"{name}: unrecognized value at {qid}: {cell.value!r}")
evidence = ""
ev_col = next((c for c, q in col_to_evidence_qid.items() if q == qid), None)
if ev_col:
ev_cell = row[ev_col - 1]
evidence = str(ev_cell.value or "").strip()
if level is not None or evidence:
r["responses"][qid] = {"level": level, "evidence": evidence}
respondents.append(r)
5. Aggregate per question (rule: mean of levels, concatenate evidences)
agg = {}
for qid in framework_qids:
levels = [r["responses"][qid]["level"] for r in respondents
if qid in r["responses"] and r["responses"][qid]["level"] is not None]
evidences = [
f"[{r['name']}]: {r['responses'][qid]['evidence']}"
for r in respondents
if qid in r["responses"] and r["responses"][qid].get("evidence")
]
if levels:
agg_level = sum(levels) / len(levels)
else:
agg_level = None
agg[qid] = {
"level": agg_level,
"evidence": "\n".join(evidences) if evidences else "",
"n_respondents": len(levels),
}
6. Generate respostas.json
import shutil, datetime
ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S")
if (KIT / "respostas.json").exists():
shutil.copy(KIT / "respostas.json", KIT / f"respostas.json.backup-{ts}")
template = json.load(open(KIT / "respostas.json"))
template["metadata"] = {
"respondent_name": f"Agregado de {len(respondents)} respondentes",
"respondent_email": "â",
"respondent_role": "Multi-respondente",
"audience": ["all"],
"organization": "<extracted from Forms or filled manually>",
"assessment_date": datetime.date.today().isoformat(),
"language": "pt-BR",
"source": "microsoft-forms-import",
"respondents": [{"name": r["name"], "email": r["email"]} for r in respondents],
}
for qid, body in agg.items():
if qid in template["responses"]:
template["responses"][qid]["level"] = body["level"]
template["responses"][qid]["evidence"] = body["evidence"]
json.dump(template, open(KIT / "respostas.json", "w"), ensure_ascii=False, indent=2)
7. Generate import log (in PT-BR, written to saida/)
# Import log â {DATE}
## Resumo
- Arquivo importado: respostas-forms.xlsx
- Respondentes: {N} ({names})
- QuestÔes processadas: {X} / 158
- QuestÔes com pelo menos 1 resposta: {Y}
- Backup do respostas.json anterior: respostas.json.backup-{TS}
## Cobertura por respondente
| Respondente | Email | Respondidas | EvidĂȘncias |
|-------------------|------------------|-------------|------------|
| Maria Tech Leader | maria@...com.br | 46 / 158 | 46 |
## Alertas
- {row N: unrecognized value at P2-C4-Q3 â "talvez" â treated as null}
- {question P3-C5-Q4 with no answer from any respondent â stays null in respostas.json}
## PrĂłximo passo
Rode `/pipeline-completo` para calcular scores e gerar relatĂłrio.
Report in chat (PT-BR)
â Importação concluĂda â respostas.json (atualizado)
â Backup: respostas.json.backup-20260508T144523
â Log: saida/import-log-2026-05-08.md
đ„ Importados:
âą 3 respondentes: Maria Tech Leader, Joao Backend SRE, Ana Security Lead
⹠142 / 158 questÔes com pelo menos 1 resposta
âą 117 evidĂȘncias capturadas
â ïž 4 alertas (ver log) â valores nĂŁo reconhecidos foram tratados como null
đŻ PrĂłximo: /pipeline-completo
Constraints
- NEVER modify
framework.json.
- ALWAYS backup
respostas.json before overwriting (.backup-<timestamp>).
- NEVER invent values: if the cell is empty or contains something unmappable, the result is
null.
- If the Excel doesn't have any header starting with
P[1-3]-C\d+-Q\d+:, stop and instruct the user to verify the format (maybe it's not a Forms export).
- Accept header variations:
P1-C1-Q1, P1-C1-Q1:, P1-C1-Q1 -, P1-C1-Q1 (...) â always use regex.
- If there's a SINGLE respondent row, aggregation is trivial (original level); if multiple, use mean (aligned with
repos/scoring.rs:354-368).
Compatibility
- Microsoft Forms export â natively supported
- Google Forms â also works (similar column format; "Likert scale 0-4" maps the same)
- Custom spreadsheet â works as long as each question header starts with
P[1-3]-C\d+-Q\d+:
- Typeform â exports CSV; convert to xlsx and adjust headers manually