Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
79dd2b1
docs(mentor): spec de design do mentor de aula
Duannee Sep 11, 2026
6007323
docs(mentor): registra as perguntas dos alunos como ativo do produto
Duannee Sep 11, 2026
542dd06
docs(mentor): nomeia o header e pareia os dois nomes do segredo interno
Duannee Sep 11, 2026
017a19e
docs(mentor): planos de implementação da ingestão e do modo mentor
Duannee Sep 11, 2026
309b27b
feat(mentor): tabelas, entidades e mappers das aulas
Duannee Sep 11, 2026
6322263
fix(mentor): snapshotar EMBEDDING_DIM na migration 0012
Duannee Sep 11, 2026
3635e4d
feat(mentor): chunking de transcrição preservando timestamps
Duannee Sep 11, 2026
bba21c3
docs(mentor): corrige teste de empacotamento do plano de ingestão
Duannee Sep 11, 2026
1024a3f
fix(mentor): algoritmo de chunking usa limite rigoroso, sem tolerância
Duannee Sep 11, 2026
d991dd7
feat(mentor): repositórios de aula e de chunk
Duannee Sep 11, 2026
24d35f5
docs(mentor): save() do repositório levanta NotFoundError, não ValueE…
Duannee Sep 11, 2026
8ed2640
fix(mentor): LessonRepository.save levanta NotFoundError, não ValueError
Duannee Sep 11, 2026
83fbdac
feat(mentor): cliente das rotas internas da borderless-api
Duannee Sep 11, 2026
39e93e0
feat(mentor): extração de áudio e transcrição com timestamps
Duannee Sep 11, 2026
da2921a
feat(mentor): máquina de estados da ingestão de uma aula
Duannee Sep 11, 2026
5cc6e34
docs(mentor): tolerância a falha cobre gravação da falha e claims obs…
Duannee Sep 11, 2026
ef4ec56
fix(mentor): garante ingestão à prova de falha no save e recupera cla…
Duannee Sep 11, 2026
b271f24
feat(mentor): comando mentor:ingest
Duannee Sep 11, 2026
6ed9e76
docs(mentor): spec §7 — commit do claim, embed(), glossário e lista d…
Duannee Sep 11, 2026
ea2ca90
fix(mentor): commit do claim, tolerância a falha e endurecimento do t…
Duannee Sep 11, 2026
2e38faa
feat(mentor): busca vetorial com escopo de aula e citação temporal
Duannee Sep 11, 2026
c3c9293
feat(mentor): tool search_lesson com escopo injetado pelo runtime
Duannee Sep 11, 2026
e107d3a
feat(mentor): modo mentor no grafo, no prompt e no contrato do turno
Duannee Sep 11, 2026
3234b3a
feat(mentor): entitlement fail-closed antes de responder
Duannee Sep 12, 2026
c0e9e45
feat(mentor): registra pergunta, cobertura e embedding no trace
Duannee Sep 12, 2026
a664847
feat(mentor): endpoint de prontidão da aula
Duannee Sep 12, 2026
424dab8
feat(mentor): aba de backlog e retenção na página de ops
Duannee Sep 12, 2026
9ccf66d
fix(mentor): painel defende de payload malformado; filtro program_slu…
Duannee Sep 12, 2026
41b56c8
fix(mentor): fecha vazamento de sessão da search_lesson e liga o kill…
Duannee Sep 12, 2026
7300f45
fix(mentor): copy do painel de ops diz "últimos 30 dias", não "nesta …
Duannee Sep 12, 2026
5c11887
docs(mentor): handoff de execução — estado, retomada, ambiente e rulings
Duannee Sep 12, 2026
b178d11
docs(mentor): handoff — revisão final do plano D capturada, próximo p…
Duannee Sep 12, 2026
f9f3a38
docs(mentor): spec §10 — status unknown esconde a aba Mentor (Ruling D9)
Duannee Sep 12, 2026
d2786cd
test(mentor): fake do turno real vira BaseChatModel que streama
Duannee Sep 12, 2026
9417153
test(mentor): embedding do trace com as dimensões reais da coluna
Duannee Sep 12, 2026
cece091
test(mentor): insert de trace recusado pelo banco não custa a resposta
Duannee Sep 12, 2026
f066ed0
test(mentor): remove as notas 'escrito mas não executado' — suítes ro…
Duannee Sep 12, 2026
9c4e7e8
docs(mentor): handoff — sessão 2: ambiente destravado, planos A–D exe…
Duannee Sep 12, 2026
19e313d
fix(notion): ler is_error do resultado da tool MCP, não isError
Duannee Sep 13, 2026
8578910
docs(mentor): drop the session handoff note before review
Duannee Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ ENABLE_SCHEDULER=true
BORDERLESS_AUTH_URL=https://api.borderlesscoding.com
ADMIN_EMAILS=

# --- Rotas internas da borderless-api (catálogo de aulas + mídia; ingestão do mentor) ---
# preencher — mesmo segredo configurado do lado da API
BORDERLESS_INTERNAL_SECRET=

# --- Navegação (agente) ---
# NAVIGATION_CATALOG_TTL_S=3600
# Cache negativo: quanto esperar antes de tentar a API de novo depois de uma falha.
Expand Down Expand Up @@ -61,6 +65,13 @@ EVAL_REPORTS_DIR=evals/reports
# O juiz é sempre OpenAI, independente de LLM_PROVIDER (ADR-0013 / spec 2026-08-03).
# JUDGE_MODEL=gpt-4.1-mini

# --- Mentor de aula (spec 2026-09-11) ---
# Kill switch de verdade (default false): sem ele ligado, tanto
# POST /conversations/ask (mode=mentor) quanto GET /lessons/{id}/status
# voltam 404 antes de qualquer trabalho — inclusive antes do entitlement.
# Precisa estar true para rodar a demo do modo mentor.
MENTOR_ENABLED=true

# --- Tracing: LangSmith (opcional; desligado por padrão) ---
LANGSMITH_TRACING=false
# LANGSMITH_API_KEY= # preencher, só se LANGSMITH_TRACING=true
Expand Down
68 changes: 68 additions & 0 deletions database/migrations/versions/0012_mentor_lessons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""lessons e lesson_chunks — base do mentor de aula (spec 2026-09-11 §4).

Revision ID: 0012_mentor_lessons
Revises: 0011_navigation_persistence
Create Date: 2026-09-11
"""

import sqlalchemy as sa
from alembic import op
from pgvector.sqlalchemy import Vector

revision = "0012_mentor_lessons"
down_revision = "0011_navigation_persistence"
branch_labels = None
depends_on = None

EMBEDDING_DIM = 1536 # settings.EMBEDDING_DIM (snapshot na migration)


def upgrade() -> None:
op.create_table(
"lessons",
sa.Column("uuid", sa.Uuid(), primary_key=True),
sa.Column("platform_video_id", sa.String(64), nullable=False),
sa.Column("program_slug", sa.String(255), nullable=False),
sa.Column("module_slug", sa.String(255), nullable=False),
sa.Column("video_slug", sa.String(255), nullable=False),
sa.Column("title", sa.String(512), nullable=False),
sa.Column("duration_seconds", sa.Integer(), nullable=True),
sa.Column("provider", sa.String(32), nullable=False),
sa.Column("provider_ref", sa.String(255), nullable=False),
sa.Column("transcript_text", sa.Text(), nullable=True),
sa.Column("transcript_status", sa.String(20), nullable=False),
sa.Column("content_hash", sa.String(64), nullable=True),
sa.Column("transcribed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failure_reason", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_lessons_platform_video_id", "lessons", ["platform_video_id"], unique=True)
op.create_index("ix_lessons_program_slug", "lessons", ["program_slug"])
op.create_index("ix_lessons_transcript_status", "lessons", ["transcript_status"])

op.create_table(
"lesson_chunks",
sa.Column("uuid", sa.Uuid(), primary_key=True),
sa.Column("lesson_id", sa.Uuid(), sa.ForeignKey("lessons.uuid", ondelete="CASCADE"), nullable=False),
sa.Column("ordinal", sa.Integer(), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("start_seconds", sa.Float(), nullable=False),
sa.Column("end_seconds", sa.Float(), nullable=False),
sa.Column("embedding", Vector(EMBEDDING_DIM), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_lesson_chunks_lesson_id", "lesson_chunks", ["lesson_id"])
# HNSW por SQL cru: o Alembic não emite `USING hnsw` com opclass.
op.execute(
"CREATE INDEX ix_lesson_chunks_embedding_hnsw "
"ON lesson_chunks USING hnsw (embedding vector_cosine_ops)"
)


def downgrade() -> None:
op.drop_index("ix_lesson_chunks_embedding_hnsw", table_name="lesson_chunks")
op.drop_table("lesson_chunks")
op.drop_table("lessons")
40 changes: 40 additions & 0 deletions database/migrations/versions/0013_mentor_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Colunas de mentor no agent_traces (spec 2026-09-11 §9.1).

Revision ID: 0013_mentor_trace
Revises: 0012_mentor_lessons
Create Date: 2026-09-11
"""

import sqlalchemy as sa
from alembic import op
from pgvector.sqlalchemy import Vector

revision = "0013_mentor_trace"
down_revision = "0012_mentor_lessons"
branch_labels = None
depends_on = None

EMBEDDING_DIM = 1536 # settings.EMBEDDING_DIM (snapshot na migration)


def upgrade() -> None:
op.add_column("agent_traces", sa.Column("lesson_id", sa.String(64), nullable=True))
op.add_column("agent_traces", sa.Column("program_slug", sa.String(255), nullable=True))
op.add_column("agent_traces", sa.Column("lesson_coverage", sa.String(16), nullable=True))
# Sem índice ANN de propósito: agrupar alguns milhares de perguntas é
# varredura, e um HNSW sobre coluna majoritariamente nula só custaria
# manutenção (spec §9.1).
op.add_column("agent_traces", sa.Column("question_embedding", Vector(EMBEDDING_DIM), nullable=True))
op.create_index("ix_agent_traces_lesson_id", "agent_traces", ["lesson_id"])
op.create_index("ix_agent_traces_program_slug", "agent_traces", ["program_slug"])
op.create_index("ix_agent_traces_lesson_coverage", "agent_traces", ["lesson_coverage"])


def downgrade() -> None:
op.drop_index("ix_agent_traces_lesson_coverage", table_name="agent_traces")
op.drop_index("ix_agent_traces_program_slug", table_name="agent_traces")
op.drop_index("ix_agent_traces_lesson_id", table_name="agent_traces")
op.drop_column("agent_traces", "question_embedding")
op.drop_column("agent_traces", "lesson_coverage")
op.drop_column("agent_traces", "program_slug")
op.drop_column("agent_traces", "lesson_id")
4 changes: 4 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ FROM python:3.13-slim
# uv para gerenciar dependências.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# ffmpeg/ffprobe: extração e fatiamento de áudio das aulas (Mentor).
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Instala dependências primeiro (cache de camadas).
Expand Down
4 changes: 4 additions & 0 deletions docs/adr/0020-fase-1-do-turno-no-corpo-sse.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Falha em gate/retrieve/refuse vira `RUN_ERROR` **depois dos passos já emitidos*

`TurnTimeline` não tem mais os três pontos. Sem atividade, a lista renderiza vazia com `aria-busy` e altura reservada de uma linha; o primeiro passo chega no round-trip HTTP. Protocolo, parser e `useAskStream` não mudam.

### Tool que precisa do banco durante `stream()` (revisão C1/mentor)

O invariante "`stream()` nunca toca o banco" descreve o que o RUNNER garante (nenhum nó do grafo entre `answer` e o fim abre sessão por conta própria); não impede uma TOOL de precisar do banco depois que o escopo 2 já fechou — `search_lesson` (modo mentor) é o primeiro caso: o modelo pode chamá-la várias vezes dentro do tool loop, todo esse loop rodando em `stream()`. A tool resolve isso abrindo o PRÓPRIO escopo curto (`async_session_scope()`, o mesmo helper do escopo 2/3) só para a duração da chamada — e a Action/Repository só podem nascer DENTRO desse `async with`, porque o Repository captura `CurrentAsyncSessionContext.get()` no `__init__`: construí-los antes (ou fora) do escopo deixaria o repositório preso à sessão já fechada do prelúdio, o que vaza conexão e falha de forma não determinística em vez de sempre. A regra prática vale para qualquer tool futura na mesma situação: se ela precisa do banco e roda em `stream()`, abre seu próprio `async_session_scope()` e constrói tudo o que capture sessão lá dentro — nunca herda a sessão de um escopo que já era do chamador.

## Consequências

**Positivas**
Expand Down
Loading