Skip to content

Repository files navigation

Cycy AI Backend

NestJS + Mastra service that powers curriculum generation and the 8-agent learning loop for Cycy.

Hackathon overview, architecture diagrams, and demo script: see the root README.

What this service owns

Domain Responsibility
Auth Clerk JWT verification, Profile / Member lookup (raw SQL on cycy tables)
Bootstrap Ingest PDF/DOCX → LLM curriculum plan → persist modules, concepts, quizzes
Learning loop Mastra learningLoopWorkflow — study through mock interview
Content bank Concept, StudyUnit, MCQ, practice, misconceptions
Mastery LearnerConceptState, PracticeAttempt, CourseScore
Session WorkflowSession per conversation
Webhook Push agent messages → cycy POST /api/internal/agent-response

System architecture (start → end)

End-to-end view of the NestJS backend: HTTP entrypoints, domain services, Mastra workflows, all 8 agents, Prisma tools, shared Postgres, LLM, and webhook delivery back to cycy.

1. Full request lifecycle

flowchart TB
  subgraph Cycy["cycy frontend"]
    User([Learner])
    UI[Next.js UI + Socket.io]
  end

  subgraph Nest["NestJS :4000 /api/v1"]
    direction TB

    subgraph Entry["HTTP controllers"]
      CurCtrl["CurriculumController<br/>bootstrap · curriculum · content"]
      ConvCtrl["ConversationsController<br/>POST /process · GET /session"]
      AssCtrl["AssessmentsController<br/>submit · explain-back"]
      Other["Progress · Courses · Health"]
    end

    Auth[ClerkAuthGuard + ProfileService]
    Queue[WorkflowQueueService<br/>async in-process jobs]

    subgraph BootstrapPipe["Curriculum bootstrap pipeline"]
      CBS[CurriculumBootstrapService]
      Ingest[MaterialIngestionService<br/>PDF/DOCX from UploadThing]
      Plan[CurriculumPlanningService]
      Persist[CurriculumPersistenceService]
      Shell[CurriculumShellService<br/>cycy Curriculum/Module/Quiz SQL]
    end

    ConvSvc[ConversationsService]
    MastraSvc[MastraService<br/>workflow start / resume]

    subgraph LearningSvc["Learning domain"]
      Concepts[ConceptService]
      Mastery[MasteryService]
      Sessions[WorkflowSessionService]
    end

    Prisma[PrismaService<br/>Neon pooler + reconnect]
    WH[CycyWebhookClient]
    LLMsvc[LlmService]
  end

  subgraph Mastra["Mastra instance — mastra.instance.ts"]
    direction TB

    subgraph Workflows["Registered workflows"]
      LLW["learningLoopWorkflow<br/>route-input → branch → orchestrate-loop"]
      CBW["curriculumBootstrapWorkflow<br/>(registered for Mastra Studio)"]
    end

    subgraph Agents["8 Mastra agents"]
      direction LR
      AG1[curriculumAgent]
      AG2[tutorAgent]
      AG3[assessmentAgent]
      AG4[progressAgent]
      AG5[reflectionAgent]
      AG6[motivationAgent]
      AG7[certificationAgent]
      AG8[interviewAgent]
    end

    subgraph Tools["Prisma-backed tools (createTool)"]
      T1[fetchStudyUnit · fetchComprehensionQuestion · fetchPracticeProblem]
      T2[logPracticeAttempt · logMisconception · updateCourseScore]
      T3[getNextConcept · getModuleConcepts · updateModuleProgress]
      T4[updateWorkflowSession · getWorkflowSession · searchKnowledge]
    end

    Store[MockStore]
  end

  subgraph External["External"]
    OpenAI[OpenAI / OpenRouter]
    DB[(Neon PostgreSQL<br/>cycy tables + backend tables)]
    WebhookRoute["cycy POST /api/internal/agent-response"]
  end

  User --> UI
  UI -->|"Bearer JWT"| Entry
  Entry --> Auth

  CurCtrl --> CBS
  CBS --> Ingest --> Plan --> Persist --> Shell
  Plan --> AG1
  Plan --> LLMsvc

  ConvCtrl --> ConvSvc --> Queue --> MastraSvc
  AssCtrl --> MastraSvc

  MastraSvc --> LLW
  LLW --> Orchestrate["orchestrate-loop.logic<br/>+ tutor-step · assessment-step · …"]

  Orchestrate --> Agents
  Agents --> Tools
  Tools --> LearningSvc
  LearningSvc --> Prisma
  Shell --> Prisma
  Ingest --> Prisma
  Persist --> Prisma
  Prisma --> DB

  Agents --> OpenAI
  LLMsvc --> OpenAI

  MastraSvc --> WH
  AssCtrl --> WH
  WH -->|"X-Internal-Secret"| WebhookRoute --> UI --> User
Loading

2. Curriculum bootstrap path

Triggered by POST /servers/:serverId/bootstrap. Runs async (202) unless BOOTSTRAP_SYNC=true.

flowchart LR
  A["POST /bootstrap"] --> B[CurriculumBootstrapService]
  B --> C{Admin + status PENDING/FAILED?}
  C -->|no| X[409 Conflict]
  C -->|yes| D[set Curriculum GENERATING]
  D --> E[MaterialIngestionService<br/>fetch PDF/DOCX · extract text]
  E --> F[CurriculumPlanningService]
  F --> G["curriculumAgent.generate()<br/>+ LlmService structured JSON fallback"]
  G --> H[CurriculumPersistenceService<br/>Concept · StudyUnit · MCQ · Quiz · Meta]
  H --> I[CurriculumShellService<br/>Module · QuizQuestion · FinalExam]
  I --> J[set Curriculum READY]
  J --> K["GET /curriculum → READY<br/>GET /curriculum/content"]

  style G fill:#6366f1,color:#fff
Loading

Tables written: cycy-owned Curriculum, Module, Quiz, QuizQuestion, FinalExam + backend-owned Concept, StudyUnit, ComprehensionQuestion, PracticeProblem, Misconception, ServerLearningMeta.

3. Learning loop — Mastra workflow

POST /conversations/:id/process and POST /assessments/* both call MastraService.process(), which starts or resumes learningLoopWorkflow using WorkflowSession state.

flowchart TB
  Start["POST /process or /assessments/submit"] --> MS[MastraService.process]
  MS --> Bind[bindLearningLoopDeps per conversationId]
  Bind --> Session[WorkflowSession getOrCreate]
  Session --> Resume{Suspended run<br/>+ step expects input?}
  Resume -->|yes| R[workflow.run.resume]
  Resume -->|no| S[workflow.run.start]

  R --> LLW[learningLoopWorkflow]
  S --> LLW

  LLW --> Route["step: route-input<br/>free_chat vs structured"]
  Route -->|free_chat| FC["step: free-chat<br/>handleFreeChat → tutorAgent"]
  Route -->|structured| OL["step: orchestrate-loop<br/>runStructuredLoop"]

  OL --> StepSwitch{WorkflowSession.step}

  StepSwitch -->|STUDY| T1["tutorAgent<br/>study unit explanation"]
  StepSwitch -->|QUICK_CHECK| A1["assessmentAgent<br/>grade MCQ"]
  StepSwitch -->|PRACTICE| A2["assessmentAgent<br/>grade practice"]
  StepSwitch -->|EXPLAIN_BACK| A3["assessmentAgent<br/>explain-back rubric"]
  StepSwitch -->|MICRO_DRILL| A4["assessmentAgent<br/>misconception drill"]
  StepSwitch -->|CONCEPT_COMPLETE| P1["progressAgent + reflectionAgent<br/>+ motivationAgent"]
  StepSwitch -->|MODULE_GATE| A5["assessmentAgent<br/>gate quiz + progressAgent unlock"]
  StepSwitch -->|GOAL_VERIFICATION| GV["runGoalVerification<br/>LLM + ServerLearningMeta"]
  StepSwitch -->|MOCK_INTERVIEW| I1["interviewAgent<br/>mock Q&A"]
  StepSwitch -->|INTERVIEW_DEBRIEF| I2["interviewAgent<br/>debrief + retry"]
  StepSwitch -->|COURSE_COMPLETE| C1["certificationAgent<br/>certificate + summary"]

  T1 & A1 & A2 & A3 & A4 & P1 & A5 & GV & I1 & I2 & C1 --> Tools[Prisma tools + Concept/Mastery services]
  FC --> Tools

  Tools --> Suspend{Needs user input?}
  Suspend -->|yes| Sus["workflow.suspend<br/>store mastraRunId in session"]
  Suspend -->|no| Done[AgentMessagePayload array]

  Suspend --> WH
  Done --> WH[CycyWebhookClient → cycy]
  WH --> Unbind[unbindLearningLoopDeps]

  style LLW fill:#6366f1,color:#fff
  style OL fill:#6366f1,color:#fff
Loading

4. Agent responsibilities

Agent Used in Purpose
curriculumAgent Bootstrap Generate full curriculum plan JSON from ingested materials
tutorAgent STUDY, FREE_CHAT Explain study units; answer off-path questions (no tools in free chat)
assessmentAgent QUICK_CHECK, PRACTICE, EXPLAIN_BACK, MICRO_DRILL, MODULE_GATE Grade MCQ, practice, explain-back, drills, module gate quizzes
progressAgent CONCEPT_COMPLETE, MODULE_GATE Advance concepts, unlock modules, update mastery
reflectionAgent CONCEPT_COMPLETE Post-concept reflection prompt
motivationAgent After assessments / concept complete Encouragement, XP milestones
certificationAgent GOAL_VERIFICATION, COURSE_COMPLETE Provisional/final certificates, goal summary
interviewAgent MOCK_INTERVIEW, INTERVIEW_DEBRIEF Job-prep mock interview Q&A and debrief

All agents (except curriculum bootstrap path) receive Prisma tools from createLearningTools() in src/ai/tools/.

5. Mastra wiring (code map)

flowchart LR
  subgraph Bootstrap["App bootstrap"]
    AppMod[AppModule] --> AiMod[AiModule]
    AiMod --> OnInit[MastraService.onModuleInit]
    OnInit --> Create[createMastraInstance]
  end

  Create --> Reg["Mastra({ agents, workflows, storage })"]
  Reg --> Agents8[8 × createXAgent]
  Reg --> WF2[learningLoopWorkflow + curriculumBootstrapWorkflow]
  Create --> Deps[LearningLoopDeps registry<br/>bind per conversationId]

  subgraph Runtime["Per request"]
    Process[MastraService.process] --> Ctx[RuntimeContext<br/>conversationId only]
    Process --> WFRun[workflow.createRunAsync]
    WFRun --> Steps["route-input · free-chat · orchestrate-loop"]
    Steps --> Logic["*-step.logic.ts files"]
    Logic --> DepsLookup[getDeps via conversationId]
  end

  style Reg fill:#6366f1,color:#fff
Loading

Deep dive (security, deferred items): docs/ARCHITECTURE.md

Setup

cd backend
pnpm install
cp .env.example .env
# Set DATABASE_URL, CLERK_SECRET_KEY, CYCY_INTERNAL_SECRET, OPENAI_API_KEY, CYCY_URL

Database (shared with cycy)

# 1. cycy migrations first
cd ../cycy && pnpm prisma migrate deploy

# 2. backend migrations
cd ../backend && pnpm prisma migrate deploy

Table ownership and migration order: docs/CONCURRENT_DB.md.

Production: use Neon pooler URL (…-pooler.….neon.tech). PrismaService automatically appends pgbouncer=true&connect_timeout=10&pool_timeout=10 and retries transient connection errors (P1017).

Seed (dev fallback only)

Product path is bootstrap from onboarding, not seed. For local workflow testing:

  1. Create a server in cycy UI
  2. Set SERVER_ID in .env
  3. Run pnpm prisma db seed

Run

pnpm dev          # http://localhost:4000
curl localhost:4000/api/v1/health

Swagger: http://localhost:4000/api/docs — Authorize with Bearer <Clerk JWT>.

API (base /api/v1)

Method Path Description
GET /health Liveness + DB ping
POST /servers/:serverId/bootstrap Generate curriculum (202 async; sync with BOOTSTRAP_SYNC=true)
GET /servers/:serverId/curriculum Status + module summary (poll until READY)
GET /servers/:serverId/curriculum/content Full generated content
GET /courses/:serverId/concepts Content bank for sidebar
GET /progress/:serverId Mastery + XP
GET /conversations/:id/session?serverId= Workflow step state
POST /conversations/:id/process Trigger / resume learning loop (202 async)
POST /assessments/submit MCQ / gate quiz answer
POST /assessments/explain-back Explain-back grading

Payloads: docs/API.md

Integration with cycy

Wiring is implemented end-to-end:

cycy onboarding  →  POST /bootstrap
user DM message  →  POST /process (LearningSession.id = conversationId)
backend workflow →  POST cycy/api/internal/agent-response (X-Internal-Secret)
cycy             →  persist Message + Socket.io emit

Guide: docs/FRONTEND_INTEGRATION.md

Project structure

src/
├── auth/           Clerk guard + ProfileService (raw SQL on cycy tables)
├── curriculum/     Bootstrap pipeline, shell queries, content API
├── materials/      PDF/DOCX ingestion from UploadThing URLs
├── ai/
│   ├── agents/     8 Mastra agents
│   ├── tools/      Prisma-backed agent tools
│   ├── workflows/  learningLoopWorkflow, curriculum plan step
│   └── mastra.service.ts
├── learning/       Concept, Mastery, WorkflowSession
├── llm/            OpenAI / OpenRouter provider
├── conversations/  POST /process
├── assessments/    Grading endpoints
├── progress/       Mastery API
├── webhooks/       Outbound client → cycy
└── prisma/         PrismaService + Neon pooler URL helper

Smoke tests

SERVER_ID=clx... pnpm test:bootstrap          # curriculum from real server
SERVER_ID=... PROFILE_ID=... pnpm test:workflow  # loop without HTTP
pnpm test                                        # Jest unit tests

Deploy (Render Docker)

  • Root directory: backend
  • Health check: /api/v1/health
  • Startup: docker-entrypoint.sh runs prisma migrate deploy then node dist/main.js

Required env: DATABASE_URL, CLERK_SECRET_KEY, CYCY_INTERNAL_SECRET, OPENAI_API_KEY, CYCY_URL, FRONTEND_URL, NODE_ENV=production

Docs index

File Purpose
docs/ARCHITECTURE.md System design, agents, security
docs/API.md REST contracts
docs/FRONTEND_INTEGRATION.md Webhook + chat integration
docs/CONCURRENT_DB.md Shared DB rules
docs/SCHEMA.md Backend Prisma models
ROADMAP.md Phased build plan

Environment

PORT=4000
DATABASE_URL=postgresql://...@ep-xxx-pooler....neon.tech/...?sslmode=require
CLERK_SECRET_KEY=sk_test_...
OPENAI_API_KEY=sk-or-v1-...          # OpenRouter supported
CYCY_URL=https://your-cycy-app.vercel.app
CYCY_INTERNAL_SECRET=long-random-string
FRONTEND_URL=https://your-cycy-app.vercel.app
BOOTSTRAP_SYNC=true                  # optional — sync bootstrap for demos
SERVER_ID=clx...                     # seed / test scripts only

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages