Classes — perfis e modelo de linguagem¶
Quem pediu a geração, o que foi gerado sobre cada pessoa, e com que credencial. Cinco tabelas, e é o único subsistema em que a plataforma escreve texto interpretado em vez de registrar o que observou.
eo_person_profiles mora na EO por ser sobre a pessoa, e aparece aqui porque é o produto desta
rodada — a mesma tabela em dois diagramas, e dito para que não pareça duplicata.
O diagrama¶
classDiagram
direction LR
class ProviderCredential {
+uuid id
+uuid tenant_id
+string provider
+string base_url
+string default_model
+binary secret
+string last_four
+uuid declared_by_user_id
+datetime validated_at
+datetime last_failure_at
+string last_failure_reason
}
class ProfileRun {
+uuid id
+uuid tenant_id
+string trigger
+uuid requested_by_user_id
+datetime started_at
+datetime finished_at
+string outcome
+string ended_reason
+string credential_last_four
+int people_selected
}
class ProfileRunEntry {
+uuid id
+uuid profile_run_id
+uuid person_id
+string outcome
+string reason
+string failure_reason
+uuid person_profile_id
+int input_tokens
+int output_tokens
}
class PersonProfile {
+uuid id
+uuid tenant_id
+uuid person_id
+datetime generated_at
+uuid requested_by_user_id
+string model
+map content
+int citations_removed
+int tasks_closed
+int tasks_open
+int tasks_with_body
+int tasks_authored_by_other
+int tasks_shared
+date period_from
+date period_to
+string baseline_verdict
}
class AutomationEvent {
+uuid id
+uuid tenant_id
+string event
+uuid actor_user_id
+datetime occurred_at
}
class Person {
<<fronteira — EO>>
+uuid id
+string name
}
ProfileRun "1" --> "0..*" ProfileRunEntry : percorre
ProfileRunEntry "0..*" --> "0..1" PersonProfile : produziu
Person "1" --> "0..*" ProfileRunEntry : é o assunto de
Person "1" --> "0..*" PersonProfile : é descrita por
ProviderCredential "1" ..> "0..*" ProfileRun : autentica
A seta de ProviderCredential para ProfileRun é pontilhada de propósito: não há FK. A
rodada guarda credential_last_four, e não credential_id — o que ela registra é com qual
credencial foi feita, de um jeito que sobrevive à credencial ser trocada. Uma FK diria a coisa
errada quando a chave fosse substituída.
Pular e falhar não são a mesma coisa¶
É o desenho central do subsistema, e está escrito em run_entry.ex:9-13:
outcome |
Afirma |
|---|---|
generated |
escreveu — e person_profile_id aponta o que escreveu |
skipped |
decidiu não escrever, e reason diz qual das três: no_material, no_new_work, observation_ended |
failed |
tentou e não conseguiu, e failure_reason é obrigatório |
Os três CHECK que tornam isso invariante, e não convenção:
| Invariante | Forma |
|---|---|
outcome fechado |
CHECK profile_run_entry_outcome_valido |
| razão só quando pulou, e de uma lista fechada | CHECK profile_run_entry_reason_valido |
| falha sempre com motivo, e motivo só em falha | CHECK profile_run_entry_falha_tem_motivo |
Sem o terceiro, uma entrada failed com failure_reason nulo seria aceita — e a rodada
diria "falhou" sem dizer no quê. É o sucesso silencioso pelo avesso.
O nulo que significa¶
| Campo nulo | Significa |
|---|---|
profile_runs.finished_at |
a rodada está em execução — é o que a FR-003 consulta para recusar a segunda, e o que o índice parcial impõe |
profile_runs.outcome |
idem: só há desfecho quando terminou. completed ou ended_early |
profile_runs.requested_by_user_id |
rodada do cron, sem pessoa que a tenha pedido (trigger = "cron") |
profile_runs.people_selected |
não medido — e o comentário no schema diz isso em vez de deixar zero passar por resposta (run.ex:33-35) |
profile_run_entries.person_profile_id |
não gerou perfil nesta entrada — porque pulou ou falhou |
profile_run_entries.reason |
não pulou |
profile_run_entries.failure_reason |
não falhou |
eo_person_profiles.baseline_verdict |
a linha de base não foi apurada para o período |
ai_provider_credentials.validated_at |
a credencial nunca foi validada contra o provedor |
ai_provider_credentials.last_failure_at |
nunca falhou desde a última gravação |
Não existe estado cancelada para a rodada, e o moduledoc explica por quê: "nenhuma linha
do código a produziria, e um estado que não acontece é um estado que quem lê precisa considerar
à toa" (run.ex:5-7).
Classe → schema → tabela → conceito¶
| Classe | Schema | Tabela | Conceito |
|---|---|---|---|
ProviderCredential |
TheBand.AI.ProviderCredential (provider_credential.ex:19) |
ai_provider_credentials |
— plataforma; provider ∈ {openai} |
ProfileRun |
TheBand.Profiles.Run (run.ex:21) |
profile_runs |
— plataforma |
ProfileRunEntry |
TheBand.Profiles.RunEntry (run_entry.ex:27) |
profile_run_entries |
— plataforma |
PersonProfile |
...EO.Schemas.PersonProfile (person_profile.ex:34) |
eo_person_profiles |
eo.competence |
AutomationEvent |
TheBand.Profiles.AutomationEvent (automation_event.ex:19) |
profile_automation_events |
— plataforma; event ∈ {enabled, disabled} |
eo_person_profiles é eo.competence — e é a escolha semântica que sustenta o subsistema:
o texto gerado é afirmação sobre competência da pessoa, não sobre a pessoa. Por isso o
CHECK eo_person_profiles_conteudo_util exige
jsonb_array_length(content -> 'habilidades') > 0: um perfil sem nenhuma habilidade não é um
perfil vazio, é um perfil que não devia ter sido gravado.
Invariantes que o diagrama não mostra¶
| Invariante | Forma |
|---|---|
| uma rodada aberta por tenant | UNIQUE profile_runs(tenant_id) WHERE finished_at IS NULL — FR-003 |
trigger ∈ {cron, manual} |
CHECK profile_run_trigger_valido |
outcome ∈ {completed, ended_early} quando há desfecho |
CHECK profile_run_outcome_valido |
people_selected não é negativo |
CHECK profile_runs_people_selected_nao_negativo |
| perfil tem ao menos uma habilidade | CHECK eo_person_profiles_conteudo_util |
evento de automação é enabled ou disabled |
CHECK profile_automation_event_valido |
a entrada é única por [rodada, pessoa] |
constraint de banco — é ela que faz a retentativa do Oban retomar em vez de gerar um segundo texto sobre o mesmo material (run_entry.ex:5-7) |
eo_person_profiles e profile_automation_events usam timestamps(updated_at: false): são
registros de ocorrência, e uma ocorrência não é atualizada.
Onde isto roda¶
config/config.exs:96 — duas filas Oban, e a separação é medida:
| Fila | Concorrência | Razão escrita no config |
|---|---|---|
perfis |
1 | cada geração leva de 25 a 60 s; paralelizar gastaria crédito em rajada sem ninguém esperando mais rápido |
rodadas |
1 | a rodada mensal percorre até 34 pessoas em sequência — de 15 a 35 min medidos. Na fila perfis ela trancaria toda geração pedida a mão |
O cron mensal é {"0 3 1 * *", TheBand.Profiles.MonthlyWorker} (config.exs:117), num fuso
só: um momento por fuso faria a mesma rodada existir várias vezes, e a proibição de
simultaneidade da FR-003 deixaria de significar.
O que ficou de fora do diagrama, e por quê¶
inserted_at/updated_at.eo_person_profiles.contentémape aparece como uma linha; a forma interna do JSON (habilidadese o resto) não é derivável do schema — só oCHECKa toca.- O prompt e o sanitizador (
profiles/prompt.ex,profiles/sanitizer.ex) não têm tabela;citations_removedé o que sobra deles no modelo. - O cliente HTTP do provedor (
lib/the_band/integrations/llm/) não foi conferido para este documento — é borda, não modelo.
Divergências encontradas¶
Nenhuma entre schema, migração e banco (comparação automática schema × colunas, 2026-09-12).
Uma ausência de dado, dita: as cinco tabelas estão vazias no banco de desenvolvimento —
ai_provider_credentials 0, profile_runs 0, profile_run_entries 0,
profile_automation_events 0, eo_person_profiles 0. O modelo está inteiro e nunca foi
exercitado com dado real neste banco. Quem for medir qualquer coisa sobre perfis precisa saber
disso antes de olhar um número.