The assistant is an AI chat for coaches inside the web app. It reads studio data through tools and answers in Hebrew. It never writes during a chat turn. When the model wants to change something it stores a proposal, and the change happens only when the coach confirms it with a second request. Source: backend/apps/core-api/src/modules/assistant/ (assistant.routes.ts, assistant.controller.ts, assistant.service.ts, assistant.schema.ts). The module has no repository file. The service calls Prisma directly.

Mounting and auth

Every handler goes through callerOf in the controller:
  • Missing studioId or userId on req.auth throws UNAUTHORIZED (401).
  • Role TRAINEE throws FORBIDDEN (403) with coaches only.
  • OWNER, HEAD_COACH and SUB_COACH are allowed.
See API overview for how the web lane builds req.auth.

Data scope

Each turn resolves the caller’s access with coachAccessFrom from middleware/coach-access.ts, using the caller’s Coach row (role, permissions, active).
  • A studio owner sees every trainee.
  • A coach whose permissions limit them to assigned trainees only sees clients that have a ClientCoach assignment to them. Their task list is limited to tasks assigned to them or tied to their trainees.
  • A coach with no Coach row, or with trainees: "all", sees the whole studio.
  • A dismissed coach (active: false) gets FORBIDDEN (403) with this team member has been removed.
Every tool query includes studioId and deletedAt: null. Admin. The service treats the caller as admin when the lane role is OWNER or the Coach row has role HEAD_COACH. Admins get the monthly revenue figure. The system prompt tells the model whether the user is an admin.

Environment variables

The assistant uses createAnthropic from @ai-sdk/anthropic and generateText from the ai package, with stopWhen: stepCountIs(8) and maxOutputTokens: 1600.

Reply shape

chat and confirm both return an AssistantReply.
  • text is Hebrew and may contain **bold** and newlines.
  • stats are number cards, at most 8.
  • list is a bullet list, at most 12 lines.
  • proposal is present when the turn produced a pending change. summary holds human readable “before to after” lines. expiresAt is an ISO timestamp 15 minutes ahead.

Endpoints

POST /v1/web/assistant/chat

Sends one message and returns the assistant’s reply. Auth: web lane, roles OWNER, HEAD_COACH, SUB_COACH.
string
required
1 to 4000 characters.
string
Accepted by the schema but not used. The controller passes only message to the service. The thread is always the caller’s single thread.
What the service does:
  1. Throws if no Anthropic provider is configured.
  2. Reads or creates the thread id in Redis and refreshes its 48 hour expiry.
  3. Increments the daily counter. Over the cap, it returns a fixed “daily limit reached” reply. That reply is not saved to history.
  4. Resolves the caller’s scope, loads the stored history and builds the message list. Only the text of earlier turns is replayed to the model. Stats, lists and proposals from earlier turns are not.
  5. Runs the model with the tool set below.
  6. Builds the reply. The model is told to finish with the reply tool, and that tool’s text, stats and list are used. If it did not call reply, the raw model text is used. If get_business_stats ran and the model sent no stats, the cards from that tool are attached anyway. If any propose_* tool ran, the last proposal is attached.
  7. Appends the user message and the reply to history.
If the model call throws, the error is logged and a fixed failure reply is returned with status 200. That turn is not saved to history. Response: 200.
A reply that carries a proposal:
Errors:

POST /v1/web/assistant/confirm

Executes a stored proposal. Auth: web lane, roles OWNER, HEAD_COACH, SUB_COACH.
string
required
The proposal.id from a chat reply.
The proposal is read with Redis GETDEL, so it can be used once. A double click, a retry or a second tab gets NOT_FOUND. The stored studioId and userId must match the caller. A mismatch throws FORBIDDEN, and the proposal is still consumed. The caller’s scope is resolved again at confirm time, so a coach who lost access to a trainee after the proposal was made cannot execute it. After execution, a “confirmed” user line and the result are appended to history. What each kind does on confirm: send_message reports soft failures in the reply text with status 200, not as errors: the studio has the TRAINER_MESSAGE notification switched off, the trainee has no registered device, WhatsApp is not connected, or the trainee has no phone. A WhatsApp message sent this way is not stored as a WhatsappMessage row. Program edits made through the assistant write straight to Program.content. This code path does not send a plan-updated notification. Response: 200.
Errors: A failed confirm does not restore the proposal. The coach has to ask again.

GET /v1/web/assistant/thread

Returns the caller’s stored conversation. Auth: web lane, roles OWNER, HEAD_COACH, SUB_COACH. Response: 200. threadId is null when no conversation exists. Messages are oldest first, up to 24.
An assistant message in history can still carry a proposal object after that proposal expired or was confirmed.

DELETE /v1/web/assistant/thread

Starts a new conversation by deleting the thread id and the history. Auth: web lane, roles OWNER, HEAD_COACH, SUB_COACH. Response: 204 with no body. Pending proposals and the daily counter are not cleared.

Tools

The model gets eleven tools. Tool inputs are validated twice: by generateText and again inside each tool. A validation or lookup failure is returned to the model as an Error: ... string so it can recover. An AppError is rethrown.

Read tools

Program selection for get_program and the program proposals: the active program of that type, or else the most recently updated of the trainee’s last five. File plans (anything isFilePlan from modules/programs/plan-kind.ts matches) are refused with an error string because they have no days, meals or targets. get_business_stats returns these cards, all limited to the caller’s scope:
  • Active trainees (clients with status ACTIVE).
  • New this month (clients created since the first of the month).
  • Open tasks.
  • Urgent tasks (open tasks with priority 2 or higher).
  • Renewals in 30 days (active clients whose endsOn falls in the next 30 days and who have no scheduled subscription ending later).
  • Check-ins today (clients whose lastCheckInAt is today).
  • Workouts this week (WorkoutLog rows in the last 7 days).
  • Monthly revenue, admins only. The sum of priceAgorot over live subscriptions in the whole studio whose span touches the current month, shown in shekels.
Day and month boundaries use the server’s local time, not the studio timezone.

Proposal tools

Each of these stores a proposal in Redis and returns it to the model. Nothing is written to the database. When several proposals are made in one turn, each is stored but only the last one is returned in the reply.

The reply tool

reply takes text (required), stats (up to 8) and list (up to 12). It writes nothing. It is how the model hands back a structured answer.

What the assistant cannot do

There are no tools for deleting anything, for charges and payments, or for team management. The system prompt tells the model to say that these are done in the app.

Redis keys

History, thread and cap keys are keyed by user id only. They do not include the studio id.

Limits

  • WhatsApp AI agent for the WhatsApp version, which runs its tools through the automation API.
  • Notification configs for the TRAINER_MESSAGE type the push channel uses.
  • WhatsApp for connecting the SmartSend key the WhatsApp channel needs.