funifier-mcp 0.2.26 → 0.2.28
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,41 +1,517 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `crossword`
|
|
2
2
|
|
|
3
|
-
**Acesso Studio:** `/studio/crossword`
|
|
4
|
-
**API Endpoint:**
|
|
3
|
+
**Acesso Studio:** Não há módulo Studio dedicado a `crossword`. Nenhuma rota `/studio/crossword` existe no código. O objeto gerado é, tipicamente, persistido/exibido pelo **chamador** (frontend / Studio) como parte de outro recurso — o backend não oferece tela nem CRUD próprios.
|
|
4
|
+
**API Endpoint:** `POST /v3/ai/build/crossword` — **único** endpoint. Classe `com.funifier.rest.v3.rest.AIRest` (`@Path("v3/ai")`), método `buildCrossword` (`@Path("/build/crossword")`, `AIRest.java:1861-1994`).
|
|
5
|
+
**Coleção MongoDB:** **Nenhuma.** O crossword é gerado em runtime e devolvido ao chamador; **não é persistido** pelo backend. Único side effect de escrita: na primeira chamada, o prompt `intro_build_crossword` é inserido em `Entity.AI_PROMPT.collection` (coleção de prompts da IA, global ao tenant) — **não** em uma coleção `crossword`.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
> **Aviso de engenharia reversa.** A documentação anterior descrevia um módulo CRUD `/v3/crossword` com tela `/studio/crossword` e coleção MongoDB `crossword`. **Nada disso existe no código.** Verificação por busca em `funifier-service/src/main/java` (`grep -rn "/crossword\|\"crossword\""`) não retorna nenhuma rota `/v3/crossword` nem mapeamento de coleção fora do pacote `engine/crossword` e de `AIRest`. O conteúdo abaixo reflete **exclusivamente** o comportamento real.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Para campanhas de marketing temáticas
|
|
14
|
-
- Para jogos educacionais
|
|
15
|
-
- Para engajamento com vocabulário específico
|
|
13
|
+
O `crossword` na plataforma Funifier é composto por **duas peças**, sem relação CRUD entre si:
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
1. **Motor de layout em memória** — pacote `com.funifier.engine.crossword`. Recebe um conjunto de palavras e produz um tabuleiro de palavras cruzadas posicionando-as com cruzamentos (interseções de letras). É um gerador algorítmico de quebra-cabeça, sem persistência.
|
|
16
|
+
2. **Endpoint de IA** — `POST /v3/ai/build/crossword` em `AIRest`. Pede ao ChatGPT que gere palavras + dicas a partir de um tema em linguagem natural, executa o motor de layout sobre essas palavras e devolve o JSON do crossword montado.
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
- [ ] Criar palavras com dicas e posições
|
|
21
|
-
- [ ] Configurar tema visual
|
|
18
|
+
Papel arquitetural:
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
- É **um entre ~30 geradores de IA** expostos por `AIRest` (`/build/quiz`, `/build/level`, `/build/challenge`, `/build/lottery`, `/build/mystery`, etc.). Todos seguem o mesmo padrão: prompt introdutório + function calling do OpenAI → POJO da plataforma.
|
|
21
|
+
- O crossword produzido é um **artefato transiente**: o backend o devolve e esquece. Cabe ao chamador decidir se salva (por exemplo, embutindo-o no `extra` de um `challenge` ou em um `database`/`custom_object`).
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
**Método:** GET
|
|
27
|
-
**Endpoint:** `/v3/crossword`
|
|
23
|
+
Dependências em runtime:
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
**
|
|
31
|
-
|
|
25
|
+
- `com.funifier.engine.system.ai` — `Prompt`, `AIManager` (`findPrompt`/`insertPrompt`), `ChatCompletionRequest`/`ChatCompletionResult`.
|
|
26
|
+
- API externa **OpenAI** (`https://api.openai.com/v1/chat/completions`, modelo `gpt-3.5-turbo-1106`).
|
|
27
|
+
- `com.funifier.engine.guid.Guid` — geração do `_id`.
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
**Método:** DELETE
|
|
35
|
-
**Endpoint:** `/v3/crossword/:id`
|
|
29
|
+
Não há, em runtime, acoplamento com `challenge`, `player`, `achievement` ou qualquer outro módulo de gamificação.
|
|
36
30
|
|
|
37
|
-
|
|
31
|
+
---
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
## 2. Arquitetura e Fluxos
|
|
34
|
+
|
|
35
|
+
### 2.1 Classes envolvidas
|
|
36
|
+
|
|
37
|
+
| Classe | Papel |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `rest.v3.rest.AIRest.buildCrossword` | Entrada REST (`POST /v3/ai/build/crossword`). Orquestra ChatGPT + motor de layout |
|
|
40
|
+
| `engine.crossword.Crossword` | POJO de saída (documento raiz) + factory estática `create(...)` |
|
|
41
|
+
| `engine.crossword.Word` | Sub-objeto de saída — uma palavra posicionada |
|
|
42
|
+
| `engine.crossword.CrossBoard` | Motor de layout. Tabuleiro `Cell[60][60]`, posicionamento e ajuste |
|
|
43
|
+
| `engine.crossword.Checks` | Validação de posições candidatas (cruzamentos, limites, adjacências) |
|
|
44
|
+
| `engine.crossword.Coordinate` | Posição candidata `{x, y, isVertical}` |
|
|
45
|
+
| `engine.crossword.AddedWord` | Palavra já posicionada `{posicaoX, posicaoY, palavra, isVertical, casas[], isPrincipal}` |
|
|
46
|
+
| `engine.crossword.Cell` | Célula do tabuleiro `{letra, isOcupada, isInativa, isPrincipal}` |
|
|
47
|
+
| `engine.crossword.WordComparator` | `Comparator<String>` — ordena por **comprimento decrescente** |
|
|
48
|
+
| `engine.crossword.Utility` | Utilitários **legados** (carregar palavras de arquivo, gerar DOCX). **Não usados** no fluxo de IA |
|
|
49
|
+
| `engine.crossword.WordList` | Agrupamento de palavras por tamanho — usado **apenas** pela geração DOCX legada |
|
|
50
|
+
| `engine.crossword.CrosswordTest` | Classe de teste/`main` standalone |
|
|
51
|
+
|
|
52
|
+
Não há `Resource` CRUD, `Service`, `Repository`/`Dao`, `Manager` nem `Scheduler`/`Job` para crossword. A única "porta" é o método `buildCrossword`.
|
|
53
|
+
|
|
54
|
+
### 2.2 Pipeline principal — `buildCrossword` (`AIRest.java:1861`)
|
|
55
|
+
|
|
56
|
+
Fluxo **síncrono** (bloqueia na chamada HTTP ao OpenAI via Unirest). Sem transação, sem persistência do resultado.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
[1] Lê do body: content (String, prompt do usuário) e current (Object, opcional)
|
|
60
|
+
[2] intro = IAManager.findPrompt("intro_build_crossword")
|
|
61
|
+
SE intro == null:
|
|
62
|
+
intro = new Prompt("intro_build_crossword", ..., <instrução hardcoded>)
|
|
63
|
+
IAManager.insertPrompt(intro) // SIDE EFFECT: grava em AI_PROMPT (global)
|
|
64
|
+
[3] Define a function OpenAI "build_crossword" (param obrigatório: crossword: string)
|
|
65
|
+
Monta system message "formatacao" instruindo o modelo a chamar a function
|
|
66
|
+
[4] ChatCompletionRequest:
|
|
67
|
+
model = gpt-3.5-turbo-1106 (MODEL_GPT_3_5)
|
|
68
|
+
system = intro.content
|
|
69
|
+
system = "Este é o crossword atual do usuário: ..." (SOMENTE se current != null)
|
|
70
|
+
system = formatacao
|
|
71
|
+
user = content
|
|
72
|
+
function_call = "auto"
|
|
73
|
+
[5] POST https://api.openai.com/v1/chat/completions
|
|
74
|
+
header Authorization = CHATGPT_TOKEN (TOKEN HARDCODED — AIRest.java:79 — ver §8)
|
|
75
|
+
[6] result = parse(response); call = result.choices[0].message.functionCall
|
|
76
|
+
json = call.getArguments()
|
|
77
|
+
[7] p = fromJson(json) // { "crossword": ... }
|
|
78
|
+
SE p.crossword é String → p = parse(p.crossword) // JSON aninhado em string
|
|
79
|
+
SENÃO → p = parse(toJson(p.crossword))
|
|
80
|
+
[8] Extrai com try/catch silencioso (apenas printStackTrace em falha):
|
|
81
|
+
title = String(p.title)
|
|
82
|
+
description = String(p.description)
|
|
83
|
+
select = parseInt(p.select) // default local = 5 se ausente/inválido
|
|
84
|
+
map = fromJson(p.words) // HashMap<palavra, dica>
|
|
85
|
+
[9] crossword = Crossword.create(title, description, map, select)
|
|
86
|
+
[10] return Callback 200 com toJson(crossword)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Observações:
|
|
90
|
+
|
|
91
|
+
- A extração do passo [8] usa `try { ... } catch (Exception e) { e.printStackTrace(); }` para cada campo. Falha de parse **não** aborta a requisição — o campo simplesmente fica com o valor anterior (`null` / `5`). Ver modos de falha em §9.
|
|
92
|
+
- O bloco inline comentado em `AIRest.java:1949-1991` é uma **duplicata** antiga da lógica de `Crossword.create` (com clamp `> 10 ? 10`). Foi substituído pela chamada a `Crossword.create` (linha 1992), que usa clamp `> 20 ? 20`. Ver §7.
|
|
93
|
+
|
|
94
|
+
#### Fluxo — `buildCrossword`
|
|
95
|
+
|
|
96
|
+
```mermaid
|
|
97
|
+
flowchart TB
|
|
98
|
+
A[POST /v3/ai/build/crossword<br/>content, current] --> B{findPrompt<br/>intro_build_crossword}
|
|
99
|
+
B -- null --> C[insertPrompt em AI_PROMPT]
|
|
100
|
+
B -- existe --> D[Monta ChatCompletionRequest]
|
|
101
|
+
C --> D
|
|
102
|
+
D --> E[POST OpenAI chat/completions<br/>token hardcoded]
|
|
103
|
+
E --> F[functionCall.arguments]
|
|
104
|
+
F --> G[Extrai title, description,<br/>select, words map]
|
|
105
|
+
G --> H[Crossword.create]
|
|
106
|
+
H --> I[200 + JSON do crossword]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Interação com OpenAI
|
|
110
|
+
|
|
111
|
+
```mermaid
|
|
112
|
+
sequenceDiagram
|
|
113
|
+
participant Cli as Chamador
|
|
114
|
+
participant AI as AIRest.buildCrossword
|
|
115
|
+
participant Mgr as AIManager
|
|
116
|
+
participant GPT as OpenAI
|
|
117
|
+
participant Eng as Crossword.create / CrossBoard
|
|
118
|
+
|
|
119
|
+
Cli->>AI: POST /v3/ai/build/crossword {content, current?}
|
|
120
|
+
AI->>Mgr: findPrompt("intro_build_crossword")
|
|
121
|
+
alt prompt ausente
|
|
122
|
+
AI->>Mgr: insertPrompt(intro) (AI_PROMPT)
|
|
123
|
+
end
|
|
124
|
+
AI->>GPT: chat/completions (function build_crossword)
|
|
125
|
+
GPT-->>AI: function_call.arguments (JSON: title, description, words, select)
|
|
126
|
+
AI->>Eng: create(title, description, words, select)
|
|
127
|
+
Eng-->>AI: Crossword {_id, title, description, words[]}
|
|
128
|
+
AI-->>Cli: 200 + JSON
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2.3 Pipeline de layout — `Crossword.create` (`Crossword.java:32`)
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
[1] numTotalPalavras = (select < 1 || select > 20) ? 20 : select // clamp [1,20], default 20
|
|
135
|
+
[2] tabuleiro = new CrossBoard(60, numTotalPalavras, true) // grid 60x60
|
|
136
|
+
[3] adicionadas = tabuleiro.gerarPalavrasAdicionadas(words.keySet(), numTotalPalavras)
|
|
137
|
+
[4] System.out de cada palavra adicionada + total // stdout
|
|
138
|
+
[5] tabuleiro.ajustarTabuleiro() // recorta bordas vazias
|
|
139
|
+
[6] tabuleiro.imprimirTabuleiro() // stdout
|
|
140
|
+
[7] PARA i = 0..adicionadas.size()-1:
|
|
141
|
+
Word(
|
|
142
|
+
number = i + 1,
|
|
143
|
+
direction = isVertical ? "vertical" : "horizontal",
|
|
144
|
+
row = posicaoX + 1, // 1-indexed
|
|
145
|
+
column = posicaoY + 1, // 1-indexed
|
|
146
|
+
hint = words.get(palavra), // dica pela chave exata
|
|
147
|
+
word = palavra
|
|
148
|
+
)
|
|
149
|
+
[8] Crossword.builder()._id(Guid.newShortGuid()) // ObjectId hex, 24 chars
|
|
150
|
+
.title(title).description(description).words(words).build()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2.4 Posicionamento das palavras — `CrossBoard`
|
|
154
|
+
|
|
155
|
+
`gerarPalavrasAdicionadas` (`CrossBoard.java:26`):
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
[1] gerarTabuleiroVazio() // 60x60 Cells vazias
|
|
159
|
+
[2] limpa faltam / adicionadas / puladas; palavrasJaTrocadas = false
|
|
160
|
+
[3] faltam = cópia das palavras; Collections.sort(faltam, WordComparator) // maior → menor
|
|
161
|
+
[4] posicionarPalavraInicial()
|
|
162
|
+
[5] posicionarPalavras()
|
|
163
|
+
[6] return adicionadas
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`posicionarPalavraInicial` (`CrossBoard.java:112`):
|
|
167
|
+
- `escolherUmaPalavra()` seleciona a primeira palavra.
|
|
168
|
+
- SE comprimento > `tamanho` (60) → **retorna sem posicionar** (caso de borda).
|
|
169
|
+
- Posiciona próximo ao **centro** com `Coordinate(x, y, isVertical=true)` — a palavra inicial é **vertical** (apesar do comentário "para ser horizontal...").
|
|
170
|
+
- Marca `adicionadas.get(0).isPrincipal = true` (palavra "principal"; sem restrição de cruzamento).
|
|
171
|
+
|
|
172
|
+
`escolherUmaPalavra` (`CrossBoard.java:128`):
|
|
173
|
+
- SE `faltam.size() > 4` → índice aleatório em `[0, faltam.size()/2)` (favorece a metade mais longa).
|
|
174
|
+
- SENÃO → `faltam.get(0)` (a mais longa restante).
|
|
175
|
+
|
|
176
|
+
`posicionarPalavras` (`CrossBoard.java:179` — versão **ativa**):
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
isVertical = true
|
|
180
|
+
maxTentativas = faltam.size() * 2
|
|
181
|
+
ENQUANTO faltam não vazio E adicionadas.size() < numPalavras E tentativas < maxTentativas:
|
|
182
|
+
palavra = escolherUmaPalavra()
|
|
183
|
+
isVertical = !isVertical // ALTERNA direção a cada iteração
|
|
184
|
+
adicionou = tentarAdicionarPalavra(palavra, isVertical)
|
|
185
|
+
SE não adicionou:
|
|
186
|
+
atualizarListasFaltamParaPuladas(palavra) // move de faltam → puladas
|
|
187
|
+
tentativas++
|
|
188
|
+
SENÃO:
|
|
189
|
+
tentativas = 0
|
|
190
|
+
SE puladas não vazio:
|
|
191
|
+
tentarPosicionarPuladas(isVertical) // tenta reencaixar puladas
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`verifDisponibilidade` (`Checks.java:13`) decide onde uma palavra pode entrar:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
posicoes = contarPossiveisPosicoes(palavra, isVertical)
|
|
198
|
+
// para CADA palavra já adicionada, verifPalavra() acha cruzamentos
|
|
199
|
+
// SOMENTE perpendiculares (palavraAdicionada.isVertical != isVertical)
|
|
200
|
+
// onde uma letra coincide
|
|
201
|
+
SE posicoes vazio → null
|
|
202
|
+
posicoes = removerPosicoesInadequadas(posicoes, palavra)
|
|
203
|
+
// mantém só as que passam em:
|
|
204
|
+
// verifLimites() → cabe dentro do tabuleiro
|
|
205
|
+
// verifCasasNoTabuleiro() → sem adjacência indevida; letras coincidem no cruzamento
|
|
206
|
+
SE posicoes vazio → null
|
|
207
|
+
SENÃO → posicoes[ Random.nextInt(posicoes.size()) ] // escolha ALEATÓRIA
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Consequência: a partir da segunda palavra, **só entram palavras que cruzam** uma palavra já posicionada compartilhando uma letra. Palavras sem cruzamento possível vão para `puladas` e podem nunca entrar.
|
|
211
|
+
|
|
212
|
+
#### Loop de posicionamento
|
|
213
|
+
|
|
214
|
+
```mermaid
|
|
215
|
+
flowchart TB
|
|
216
|
+
S[faltam ordenado por tamanho] --> Ini[posicionarPalavraInicial<br/>vertical, centro, isPrincipal]
|
|
217
|
+
Ini --> L{"faltam vazio? OR<br/>adicionadas >= numPalavras? OR<br/>tentativas >= max?"}
|
|
218
|
+
L -- continuar --> P[escolherUmaPalavra]
|
|
219
|
+
P --> T[isVertical = !isVertical]
|
|
220
|
+
T --> V[verifDisponibilidade<br/>cruzamentos perpendiculares]
|
|
221
|
+
V -- posição válida --> Add[adicionarPalavraTabuleiro<br/>tentativas = 0]
|
|
222
|
+
V -- nenhuma --> Sk[move para puladas<br/>tentativas++]
|
|
223
|
+
Add --> R[tentarPosicionarPuladas]
|
|
224
|
+
Sk --> R
|
|
225
|
+
R --> L
|
|
226
|
+
L -- parar --> Fim[adicionadas]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`ajustarTabuleiro` (`CrossBoard.java:49`): calcula `minX/minY/maxX/maxY` das palavras adicionadas, desloca todas para a origem (`posicaoX -= minX`, `posicaoY -= minY`) e recalcula `tamanho`. É depois desse passo que `row = posicaoX + 1` e `column = posicaoY + 1` ficam relativos ao canto do tabuleiro recortado.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 3. Estrutura dos Objetos
|
|
234
|
+
|
|
235
|
+
### 3.1 `Crossword` — documento raiz (saída)
|
|
236
|
+
|
|
237
|
+
`com.funifier.engine.crossword.Crossword`. **Não persistido pelo backend** — é o corpo da resposta.
|
|
238
|
+
|
|
239
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
240
|
+
|---|---|---|---|---|
|
|
241
|
+
| `_id` | String | `Guid.newShortGuid()` | Não (auto) | Gerado por `new org.bson.types.ObjectId().toString()` → **hex de 24 caracteres**, apesar do tipo `String`. Mapeado por `@JsonProperty("_id")` |
|
|
242
|
+
| `title` | String | `"null"` (string) se ausente | Não | Título vindo do JSON gerado pela IA. Se a IA omitir, `String.valueOf(p.get("title"))` produz a **string literal** `"null"` (não o valor nulo) — ver §9 |
|
|
243
|
+
| `description` | String | `"null"` (string) se ausente | Não | Descrição vinda do JSON da IA. Mesmo efeito `"null"` literal se ausente |
|
|
244
|
+
| `words` | List<`Word`> | `[]` | Sim (significativo) | Palavras efetivamente posicionadas no tabuleiro. **Tamanho ≤ `select`** (pode ser menor) |
|
|
245
|
+
|
|
246
|
+
Anotações: `@Data @Builder(toBuilder=true) @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown=true)`. `words` é `@Singular`.
|
|
247
|
+
|
|
248
|
+
**Campos computados / não persistidos:** todo o objeto é transiente — nada vai ao MongoDB por este fluxo.
|
|
249
|
+
|
|
250
|
+
### 3.2 `Word` — sub-entidade (saída)
|
|
251
|
+
|
|
252
|
+
`com.funifier.engine.crossword.Word`. Um item de `Crossword.words`.
|
|
253
|
+
|
|
254
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
255
|
+
|---|---|---|---|---|
|
|
256
|
+
| `number` | int | — | Sim | Sequencial `1..N` na ordem de adição ao tabuleiro (não é ordem de leitura/grade) |
|
|
257
|
+
| `direction` | String | — | Sim | `"vertical"` ou `"horizontal"`. Derivado de `AddedWord.isVertical` |
|
|
258
|
+
| `row` | int | — | Sim | `posicaoX + 1` — **1-indexed**. Eixo das linhas (cresce para baixo) |
|
|
259
|
+
| `column` | int | — | Sim | `posicaoY + 1` — **1-indexed**. Eixo das colunas |
|
|
260
|
+
| `hint` | String | — | Sim | Dica da palavra (`words.get(palavra)`, busca pela chave exata) |
|
|
261
|
+
| `word` | String | — | Sim | A própria palavra |
|
|
262
|
+
|
|
263
|
+
**Semântica de coordenadas:** `x` é a linha, `y` é a coluna. `isVertical == true` → a palavra se estende ao longo de `x` (linhas, de cima para baixo). `row`/`column` são **1-indexados** na resposta (o motor é 0-indexed internamente).
|
|
264
|
+
|
|
265
|
+
### 3.3 Estruturas internas (não saem na resposta)
|
|
266
|
+
|
|
267
|
+
| Classe | Campos | Observação |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `Cell` | `letra` (char), `isOcupada` (bool), `isInativa` (bool), `isPrincipal` (bool) | `isInativa` é **lido** em `Checks.verifPalavra` mas **nunca escrito** em lugar nenhum → sempre `false` → ramo morto (ver §7) |
|
|
270
|
+
| `AddedWord` | `posicaoX`, `posicaoY`, `palavra`, `isVertical`, `casas[]`, `isPrincipal` | `isPrincipal` marca a primeira palavra; **não** é exportado para `Word` |
|
|
271
|
+
| `Coordinate` | `x`, `y`, `isVertical` | Posição candidata produzida por `Checks` |
|
|
272
|
+
| `CrossBoard` | `tamanho`, `numPalavras`, `faltam`, `adicionadas`, `puladas`, `tabuleiro[][]`, ... | Estado do motor; descartado ao fim de `create` |
|
|
273
|
+
|
|
274
|
+
### 3.4 Estrutura do JSON gerado pela IA (entrada do motor)
|
|
275
|
+
|
|
276
|
+
O ChatGPT é instruído (prompt `intro_build_crossword`) a produzir um JSON com:
|
|
277
|
+
|
|
278
|
+
| Campo | Tipo | Descrição (conforme o prompt) |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| `title` | String | Título do jogo |
|
|
281
|
+
| `description` | String | Descrição |
|
|
282
|
+
| `words` | Map<palavra, dica> | Pares palavra→dica. O prompt manda gerar **o dobro** das palavras pedidas, limitado a **40** |
|
|
283
|
+
| `select` | int | Quantas palavras o usuário quer ver no jogo, limitado a **20** no prompt |
|
|
284
|
+
|
|
285
|
+
Notas do prompt: as palavras devem ser **MAIÚSCULAS** por padrão (outro formato só se o usuário pedir). O pool maior (`words`) que `select` dá folga ao algoritmo para achar boas interseções.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 4. Endpoints
|
|
290
|
+
|
|
291
|
+
### 4.1 `POST /v3/ai/build/crossword`
|
|
292
|
+
|
|
293
|
+
| Aspecto | Detalhe |
|
|
294
|
+
|---|---|
|
|
295
|
+
| Finalidade | Gerar um crossword (palavras + dicas + layout) a partir de um tema em linguagem natural, via IA |
|
|
296
|
+
| Autenticação | Bearer token (`@BeanParam AuthBean`) |
|
|
297
|
+
| Consumes | `application/json` |
|
|
298
|
+
| Produces | `application/json; charset=UTF-8` |
|
|
299
|
+
| Full replace ou patch | N/A — não é CRUD. Gera e devolve; **não persiste** |
|
|
300
|
+
| Idempotência | **Não** — saída varia entre chamadas idênticas (ver §5.2) |
|
|
301
|
+
|
|
302
|
+
**Body params:**
|
|
303
|
+
|
|
304
|
+
| Param | Tipo | Obrigatório | Descrição |
|
|
305
|
+
|---|---|---|---|
|
|
306
|
+
| `content` | String | Sim (de fato) | Pedido em linguagem natural (tema, quantidade, idioma…). Vira a mensagem `user` enviada ao modelo |
|
|
307
|
+
| `current` | Object | Não | Crossword atual do usuário. Se presente, é injetado como contexto (`system`) para o modelo **refinar/editar** em vez de criar do zero |
|
|
308
|
+
|
|
309
|
+
**Comportamento real:**
|
|
310
|
+
|
|
311
|
+
- O backend **não valida** `content`; se ausente, `content = null` e o modelo recebe uma mensagem `user` nula (comportamento indefinido do parse OpenAI).
|
|
312
|
+
- O resultado **não é salvo**. O chamador é responsável por persistir.
|
|
313
|
+
- `select` sofre dois níveis de tratamento: default local `5` em `AIRest` (se a IA omitir/enviar inválido) e clamp `[1,20]` (else `20`) em `Crossword.create`. Ver §5.1.
|
|
314
|
+
- Logs vão para **stdout** (`System.out.println`), não para logger estruturado.
|
|
315
|
+
|
|
316
|
+
**Exemplo de request:**
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
POST /v3/ai/build/crossword
|
|
320
|
+
Authorization: Bearer eyJhbGciOi...
|
|
321
|
+
Content-Type: application/json
|
|
322
|
+
|
|
323
|
+
{
|
|
324
|
+
"content": "Crie um crossword sobre frutas típicas do Brasil com 5 palavras"
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Exemplo de response (200):**
|
|
329
|
+
|
|
330
|
+
```json
|
|
331
|
+
{
|
|
332
|
+
"_id": "665f0a91c8b0000000abcdef",
|
|
333
|
+
"title": "Desafio de Frutas Brasileiras",
|
|
334
|
+
"description": "Encontre as frutas típicas do Brasil.",
|
|
335
|
+
"words": [
|
|
336
|
+
{ "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica usada em sucos", "word": "AÇAÍ" },
|
|
337
|
+
{ "number": 2, "direction": "horizontal", "row": 13, "column": 6, "hint": "Fruta pequena e roxa, nativa do Brasil", "word": "JABUTICABA" },
|
|
338
|
+
{ "number": 3, "direction": "vertical", "row": 10, "column": 11, "hint": "Fruta em forma de estrela quando cortada", "word": "CARAMBOLA" }
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
> O número, a posição e a quantidade de palavras variam a cada chamada — o exemplo é ilustrativo.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 5. Regras de Negócio
|
|
348
|
+
|
|
349
|
+
Regras presentes no **código** e ausentes de qualquer schema:
|
|
350
|
+
|
|
351
|
+
### 5.1 `select` — dois níveis de tratamento
|
|
352
|
+
|
|
353
|
+
- Em `AIRest.buildCrossword`: `int select = 5;` e só é sobrescrito se `parseInt(p.select)` tiver sucesso. Logo, **omissão da IA → `select = 5`**.
|
|
354
|
+
- Em `Crossword.create`: `numTotalPalavras = (select < 1 || select > 20) ? 20 : select`. Ou seja, valores fora de `[1,20]` viram **20** (não 5). Exemplos: `0 → 20`, `30 → 20`, `7 → 7`.
|
|
355
|
+
|
|
356
|
+
### 5.2 Saída não-determinística
|
|
357
|
+
|
|
358
|
+
`escolherUmaPalavra` e `verifDisponibilidade` usam `java.util.Random` **sem seed**. A mesma entrada (mesmo `content` e até o mesmo conjunto de palavras) gera **tabuleiros diferentes** em chamadas distintas. Não há como reproduzir um layout.
|
|
359
|
+
|
|
360
|
+
### 5.3 Pode posicionar **menos** palavras que `select`
|
|
361
|
+
|
|
362
|
+
O laço para quando `faltam` esvazia, quando `adicionadas.size() == numPalavras` **ou** quando `tentativas >= faltam.size() * 2`. Palavras sem cruzamento válido vão para `puladas` e podem nunca entrar. Portanto `words.length ≤ select` — e frequentemente **menor**, sobretudo com poucas palavras no pool ou letras pouco compartilhadas.
|
|
363
|
+
|
|
364
|
+
### 5.4 Primeira palavra é "principal" e sem restrição
|
|
365
|
+
|
|
366
|
+
A palavra inicial é posicionada **vertical**, próxima ao centro, e marcada `isPrincipal = true`. É a única que entra sem precisar cruzar outra. Se ela for maior que 60 caracteres, **nada** é posicionado (a inicial é abortada e nenhuma outra tem âncora).
|
|
367
|
+
|
|
368
|
+
### 5.5 Direção alternada
|
|
369
|
+
|
|
370
|
+
`isVertical` é invertido a cada iteração do laço (`isVertical = !isVertical`), tendendo a alternar horizontal/vertical entre tentativas consecutivas.
|
|
371
|
+
|
|
372
|
+
### 5.6 Cruzamento obrigatório e perpendicular
|
|
373
|
+
|
|
374
|
+
`Checks.verifPalavra` só considera interseções com palavras de **direção oposta** (`palavraAdicionada.isVertical != isVertical`) que **compartilham uma letra**. `verifCasasNoTabuleiro` rejeita adjacências indevidas (palavras paralelas coladas) e exige letras coincidentes nos cruzamentos.
|
|
375
|
+
|
|
376
|
+
### 5.7 Multi-tenant
|
|
377
|
+
|
|
378
|
+
- Autenticação por `AuthBean` (Bearer). O crossword gerado **não é escopado** a nada (não persiste).
|
|
379
|
+
- O prompt `intro_build_crossword` é inserido **uma vez** na coleção `AI_PROMPT` do tenant. É **global** — não há variação por usuário. Editá-lo no banco altera o comportamento de **todas** as gerações daquele tenant.
|
|
380
|
+
|
|
381
|
+
### 5.8 Falhas de parse são silenciosas
|
|
382
|
+
|
|
383
|
+
Cada extração de campo em `buildCrossword` é envolvida por `try/catch` que apenas faz `printStackTrace()`. Uma resposta malformada do modelo não retorna erro 4xx — degrada para valores default/`null`, podendo causar `NullPointerException` mais adiante (ver §9).
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 6. Comportamentos Automáticos
|
|
388
|
+
|
|
389
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
390
|
+
|---|---|---|---|
|
|
391
|
+
| Inserção do prompt `intro_build_crossword` | Primeira chamada ao endpoint (quando `findPrompt` retorna `null`) | Cria o prompt introdutório hardcoded | **Sim** — coleção `AI_PROMPT` (global ao tenant) |
|
|
392
|
+
| Geração de `_id` | `Crossword.builder()` em `create` | `ObjectId` hex de 24 chars | Não (só no objeto devolvido) |
|
|
393
|
+
| Clamp de `select` | `Crossword.create` | Limita a `[1,20]`; fora disso → 20 | Não |
|
|
394
|
+
| Recorte do tabuleiro | `ajustarTabuleiro()` | Remove bordas vazias, normaliza `row`/`column` à origem | Não |
|
|
395
|
+
| Marcação `isPrincipal` | `posicionarPalavraInicial` | Primeira palavra marcada como principal (não exportado) | Não |
|
|
396
|
+
| Logs em stdout | `Crossword.create` e `buildCrossword` | Imprime palavras, tabuleiro, modelo e uso de tokens no stdout | Não (apenas log de processo) |
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 7. Suportado vs NÃO Suportado
|
|
401
|
+
|
|
402
|
+
### ✅ Suportado
|
|
403
|
+
|
|
404
|
+
- Geração de crossword por **tema em linguagem natural** (`content`).
|
|
405
|
+
- **Refinamento** de um crossword existente (`current` injetado como contexto da IA).
|
|
406
|
+
- Layout automático com cruzamentos perpendiculares e validação de adjacência.
|
|
407
|
+
- Clamp de `select` para `[1,20]`.
|
|
408
|
+
- Saída JSON pronta para o frontend: `_id`, `title`, `description`, `words[]` com `number`, `direction`, `row`, `column`, `hint`, `word`.
|
|
409
|
+
|
|
410
|
+
### ❌ NÃO Suportado
|
|
411
|
+
|
|
412
|
+
- **CRUD `/v3/crossword`** (GET / POST / PUT / DELETE) — **não existe**. Verificado por grep em `funifier-service`. O único endpoint é `POST /v3/ai/build/crossword`.
|
|
413
|
+
- **Tela `/studio/crossword`** — não existe módulo Studio dedicado.
|
|
414
|
+
- **Coleção MongoDB `crossword`** — o objeto **não é persistido**. Não há listagem, edição nem remoção pelo backend.
|
|
415
|
+
- **Determinismo / reprodutibilidade** — saída varia entre chamadas (uso de `Random` sem seed).
|
|
416
|
+
- **Garantia de posicionar todas as `select` palavras** — pode posicionar menos.
|
|
417
|
+
- **`PUT`/atualização** de um crossword — inexistente (não há recurso para atualizar).
|
|
418
|
+
- `gerarTabuleiro()` + `Utility.carregarPalavras` (carga de `src/com/diego/cruzadas/resources/palavras.txt`) — código **legado** que lê palavras de arquivo local; **não** é alcançado pelo fluxo de IA (que usa `gerarPalavrasAdicionadas`).
|
|
419
|
+
- `posicionarPalavras___OLD()` — algoritmo antigo de posicionamento; **nunca chamado**.
|
|
420
|
+
- Geração de DOCX (`Utility.gerarTabelaComum`, `salvarArquivoNormal`, `salvarArquivoRespondido`) — **comentada** no fonte.
|
|
421
|
+
- `Cell.isInativa` — campo **declarado e lido** (`Checks.verifPalavra`) mas **nunca atribuído** → sempre `false` → o ramo que o consulta é morto.
|
|
422
|
+
- Bloco inline comentado em `AIRest.java:1949-1991` — duplica `Crossword.create` com clamp antigo `> 10 ? 10` (limite depois elevado para 20 em `Crossword.create`).
|
|
423
|
+
- `WordList`, `Utility.mostrarPalavras`, `Utility.separarPalavrasTamanho`, `Utility.setTableAlign` — auxiliares **somente** da geração DOCX legada.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 8. Segurança e Permissões
|
|
428
|
+
|
|
429
|
+
- **Autenticação:** Bearer token via `@BeanParam AuthBean`. Sem autorização granular específica para crossword.
|
|
430
|
+
- **🔴 Token OpenAI hardcoded no fonte — `AIRest.java:79`:**
|
|
431
|
+
```java
|
|
432
|
+
public static String CHATGPT_TOKEN = "Bearer sk-...<chave exposta>...";
|
|
433
|
+
```
|
|
434
|
+
Segredo de API exposto em código versionado. Qualquer pessoa com acesso ao repositório pode consumir a chave (custo financeiro direto + risco de abuso). **Recomendação:** revogar a chave, movê-la para variável de ambiente / secret manager e remover do histórico do Git.
|
|
435
|
+
- **Prompt injection:** `content` e `current` são repassados **diretamente** ao modelo como mensagens. Um usuário pode tentar manipular/escapar o prompt (jailbreak). O impacto é limitado — a saída é apenas um JSON de crossword montado por `Crossword.create` — mas conteúdo arbitrário pode aparecer em `title`/`description`/`hint`.
|
|
436
|
+
- **Isolamento de tenant:** o prompt `intro_build_crossword` é **global** ao tenant (coleção `AI_PROMPT`). O crossword gerado não é escopado a nada por não ser persistido.
|
|
437
|
+
- **Sem rate limiting** visível no método → cada chamada é uma requisição paga ao OpenAI; não há proteção contra abuso/loop de requisições.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 9. Observabilidade e Troubleshooting
|
|
442
|
+
|
|
443
|
+
**Diagnóstico:**
|
|
444
|
+
|
|
445
|
+
- Não há logger estruturado. O método imprime no **stdout** do servidor:
|
|
446
|
+
- `ChatGPT Build : Crossword`, `ChatGPT Model : gpt-3.5-turbo-1106`, `ChatGPT Usage : {...}` (tokens), o JSON cru retornado, cada palavra adicionada e o tabuleiro ASCII (`imprimirTabuleiro`).
|
|
447
|
+
- Para verificar se o módulo funciona: chamar o endpoint com um `content` simples e inspecionar a resposta (status 200 + `words[]` não vazio).
|
|
448
|
+
|
|
449
|
+
**Erros comuns e causas:**
|
|
450
|
+
|
|
451
|
+
| Sintoma | Causa provável |
|
|
452
|
+
|---|---|
|
|
453
|
+
| `500` / `NullPointerException` | `words` não parseado (`map == null`) → `Crossword.create` faz `wordsRepository.keySet()` sobre `null`. Ocorre quando a IA não retorna o campo `words` ou retorna formato inesperado |
|
|
454
|
+
| `500` no parse do retorno | OpenAI não retornou `function_call` (ex.: token inválido/expirado, indisponibilidade) → `message.getFunctionCall()` é `null` |
|
|
455
|
+
| `title`/`description` valendo a string `"null"` | A IA omitiu o campo; `String.valueOf(p.get("title"))` produz `"null"` literal |
|
|
456
|
+
| `words` com menos itens que o pedido | Normal — palavras sem cruzamento válido foram puladas, ou `tentativas >= faltam.size()*2` (ver §5.3) |
|
|
457
|
+
| Resposta sem nenhuma palavra | Pool com 1 palavra e ela maior que 60 chars, ou nenhuma interseção encontrada após a inicial |
|
|
458
|
+
|
|
459
|
+
**Comandos úteis:**
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
POST /v3/ai/build/crossword { "content": "crossword sobre capitais do Brasil, 8 palavras" }
|
|
463
|
+
POST /v3/ai/build/crossword { "content": "...", "current": { ...crossword existente... } }
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Inspeção do prompt global (via API de IA/prompts do tenant), caso o comportamento da geração precise ser ajustado: procurar o documento `_id = "intro_build_crossword"` na coleção `AI_PROMPT`.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 10. Exemplos Práticos
|
|
471
|
+
|
|
472
|
+
### Exemplo mínimo
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
POST /v3/ai/build/crossword
|
|
476
|
+
{ "content": "Crie um crossword sobre animais da savana com 5 palavras" }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Devolve um `Crossword` com `_id`, `title`, `description` e até 5 palavras posicionadas.
|
|
480
|
+
|
|
481
|
+
### Exemplo avançado — refinar um crossword existente
|
|
482
|
+
|
|
483
|
+
```json
|
|
484
|
+
POST /v3/ai/build/crossword
|
|
485
|
+
{
|
|
486
|
+
"content": "Troque as palavras muito difíceis por outras mais fáceis e mantenha o tema",
|
|
487
|
+
"current": {
|
|
488
|
+
"title": "Desafio de Frutas Brasileiras",
|
|
489
|
+
"description": "Encontre as frutas típicas do Brasil.",
|
|
490
|
+
"words": [
|
|
491
|
+
{ "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica", "word": "AÇAÍ" }
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
O `current` é injetado como contexto `system`; o modelo gera um novo conjunto de palavras/dicas e o motor remonta o tabuleiro.
|
|
498
|
+
|
|
499
|
+
### Anti-pattern (o que NÃO fazer)
|
|
500
|
+
|
|
501
|
+
- ❌ Chamar `GET /v3/crossword` ou `DELETE /v3/crossword/:id` — **não existem**. Não há CRUD.
|
|
502
|
+
- ❌ Esperar que o backend **salve** o crossword. Ele apenas devolve; persistência é responsabilidade do chamador.
|
|
503
|
+
- ❌ Depender da **mesma saída** para a mesma entrada — o layout é aleatório.
|
|
504
|
+
- ❌ Pedir `select` muito alto achando que vem mais palavras — acima de 20 vira 20, e o tabuleiro ainda pode posicionar menos.
|
|
505
|
+
- ❌ Confiar no token OpenAI hardcoded em produção — deve ser externalizado (ver §8).
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Checklist de Configuração
|
|
510
|
+
|
|
511
|
+
- [ ] Token OpenAI **válido** disponível (hoje hardcoded em `AIRest.java:79` — externalizar e revogar a chave exposta).
|
|
512
|
+
- [ ] `content` descreve **tema + quantidade** (e idioma/formato, se necessário) de forma clara.
|
|
513
|
+
- [ ] O chamador **persiste** o crossword retornado (o backend não persiste).
|
|
514
|
+
- [ ] Não depender de **determinismo** — tratar cada resposta como nova.
|
|
515
|
+
- [ ] `select` entre **1 e 20** (fora disso o backend força 20).
|
|
516
|
+
- [ ] Tratar o caso de **`words` vir vazio ou menor** que o solicitado.
|
|
517
|
+
- [ ] Armadilha silenciosa: se a IA **omitir `words`**, a geração pode lançar `NullPointerException` (500) — validar o retorno.
|