Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
179 changes: 178 additions & 1 deletion BackendAcademy/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,181 @@ When integrating a frontend built with **shadcn/ui**, backend endpoints should p
}
]
}
```
```

---

# AI Hints — How They Are Generated and Used

The AI subsystem lives in `src/ai/` and exposes three capabilities to the rest of the platform: **chat-based mentoring**, **graduated task hints**, and **AI pre-scoring of code submissions**. This section focuses on hints specifically, then covers the supporting pieces.

## Architecture Overview

```
Client (frontend / mobile)
POST /ai/hint ← AiController
AiService.getHint() ← business logic, hint store, difficulty routing
├── [hint found in store] → return stored hint + increment usedCount
└── [no hint in store] → return generic fallback message
```

When `AI_PROVIDER=claude` (or `openai`) is set, the `processChatRequest` path uses the provider to generate dynamic responses. The hint path currently uses a pre-seeded in-memory store — dynamic AI-generated hints are the planned Phase 2 upgrade (see below).

## Request / Response Shape

### POST `/ai/hint`

**Request body** (`GetHintDto`):

```json
{
"challengeId": "sample-challenge-001",
"userId": "user-abc",
"difficulty": 2
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `challengeId` | string | ✅ | Identifier of the task or challenge |
| `userId` | string | ✅ | Identifier of the requesting learner |
| `difficulty` | number | ❌ | Hint tier to request (1 = most gentle, 3 = most specific). Defaults to 1 |

**Response body** (`AiHintResponse`):

```json
{
"hint": "Consider edge cases - empty, null, or out-of-range inputs.",
"hintId": "3f2c1a...",
"difficulty": 2
}
```

| Field | Type | Description |
|---|---|---|
| `hint` | string | The hint text shown to the learner |
| `hintId` | string | UUID of the specific hint record (for analytics) |
| `difficulty` | number | Difficulty tier that was actually served (may differ from request if the requested tier was unavailable) |

If no hints exist for the given `challengeId`, the API returns HTTP 200 with a generic fallback:

```json
{
"hint": "No hints available for this challenge yet. Keep trying!",
"hintId": "<generated-uuid>",
"difficulty": 1
}
```

## Hint Difficulty Tiers

Hints are designed to be **graduated** — each tier reveals progressively more information so learners are guided without being spoiled.

| Tier | Intent | Example |
|---|---|---|
| **1** — Conceptual nudge | Reframe the problem; no implementation detail | `"Start by understanding the problem requirements thoroughly."` |
| **2** — Edge-case reminder | Point toward gotchas without giving code | `"Consider edge cases — empty, null, or out-of-range inputs."` |
| **3** — Algorithmic direction | Suggest an approach or pattern | `"Implement brute-force first, then optimize."` |

When a learner requests tier 2 but only tier 1 is stored, `AiService.getHint()` falls back to the first available hint for that challenge rather than returning nothing.

## How Hints Are Stored and Seeded

`AiService` maintains an in-memory `Map<challengeId, Hint[]>` called `hints`. On startup, `initializeSampleHints()` pre-populates it with the three sample tiers for `"sample-challenge-001"`.

```
Hint {
id – UUID
challengeId – which challenge this hint belongs to
hint – hint text
difficulty – tier number (1–3)
usedCount – incremented each time the hint is served
}
```

`usedCount` is tracked so the analytics layer can identify which hints learners reach most often — a signal that difficulty calibration may need adjustment on a given challenge.

In production, this in-memory store will be replaced by a database table. The service interface (`getHint`, `AiHintResponse`) will remain unchanged.

## AI Provider Wiring

The hint system currently runs entirely off the in-memory store, so it works without any API key configured. The full AI-powered chat path uses a pluggable provider selected at startup:

```
AI_PROVIDER=claude → ClaudeProvider (Anthropic Messages API)
AI_PROVIDER=openai → OpenaiProvider (OpenAI Chat Completions API)
(unset / other) → null provider (deterministic fallback responses)
```

The factory is defined in `AiModule` and injects the chosen provider into `AiService` via the `AI_PROVIDER` token:

```typescript
// src/ai/ai.module.ts
const aiProviderFactory = {
provide: AI_PROVIDER,
useFactory: (configService: ConfigService) => {
const provider = configService.get<string>('AI_PROVIDER');
if (provider === 'openai') return new OpenaiProvider(configService);
if (provider === 'claude') return new ClaudeProvider(configService);
return null; // ← fallback, no external calls
},
inject: [ConfigService],
};
```

`ClaudeProvider` calls `POST https://api.anthropic.com/v1/messages` using the model specified by `AI_MODEL` (default: `claude-sonnet-4-20250514`), with `AI_MAX_TOKENS` (default: 4096) and `AI_TEMPERATURE` (default: 0.7).

## Environment Variables

| Variable | Default | Description |
|---|---|---|
| `AI_PROVIDER` | _(none)_ | `claude` or `openai`; omit to use offline fallback |
| `ANTHROPIC_API_KEY` | _(none)_ | Required when `AI_PROVIDER=claude` |
| `OPENAI_API_KEY` | _(none)_ | Required when `AI_PROVIDER=openai` |
| `AI_MODEL` | `claude-sonnet-4-20250514` | Model name passed to the provider |
| `AI_MAX_TOKENS` | `4096` | Maximum tokens per AI response |
| `AI_TEMPERATURE` | `0.7` | Sampling temperature (0 = deterministic, 1 = creative) |

Copy `.env.example` and fill in the relevant keys:

```bash
cp .env.example .env
```

## Related Endpoints

| Method | Path | Description |
|---|---|---|
| `POST` | `/ai/hint` | Fetch a graduated hint for a challenge |
| `POST` | `/ai/chat` | Send a free-form message to the AI Mentor |
| `POST` | `/ai/pre-score` | Submit code for an AI pre-score before tutor review |
| `GET` | `/ai/history/:userId` | Retrieve a user's full chat history |

### POST `/ai/chat`

Sends a conversational message to the AI Mentor. The system prompt is fixed to `"You are a helpful Rust programming tutor."` The full message history per user is stored in memory and returned by `GET /ai/history/:userId`.

Request body fields: `message` (string), `userId` (string), optional `context` (object).

### POST `/ai/pre-score`

Performs a static analysis pre-score on submitted Rust code before it enters the tutor review queue. The heuristic checks for:

- Presence of `fn main()` (+15 pts)
- Use of functions with non-trivial line count (+15 pts)
- Presence of comments (+10 pts)
- Code length > 20 lines (+10 pts)

Base score is 50. Final score is clamped to [0, 100]. A `confidence` of `0.7` is always reported in the current placeholder — this will be replaced by a model-calibrated confidence value once the full AI grading pipeline is wired in.

## Planned Enhancements (Phase 2)

- **Dynamic hint generation** — when no hint is stored for a `challengeId`, fall through to the AI provider to generate one on-demand using the task description as context.
- **Per-user hint gating** — track how many hints a learner has consumed per challenge and reduce XP payout accordingly.
- **Database persistence** — migrate `hints` and `chatHistory` maps to PostgreSQL via the Supabase client.
- **Streaming responses** — switch the chat endpoint to Server-Sent Events for real-time token streaming.
17 changes: 17 additions & 0 deletions BackendAcademy/src/rewards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ export { RewardsService } from './rewards.service';
export { RewardsController } from './rewards.controller';
export { StreakService } from './streak.service';
export { StreakController } from './streak.controller';
export { ReferralService } from './referral.service';
export { ReferralController } from './referral.controller';
export {
MAX_LEVEL,
levelForXp,
xpThresholdForLevel,
xpToNextLevel,
} from './rewards.constants';
export {
REFERRAL_BONUS_XLM,
REFERRAL_CURRENCY,
REFERRAL_EXPIRY_DAYS,
MAX_PENDING_REFERRALS_PER_USER,
} from './referral.constants';
export type {
LevelThreshold,
UserProgressionResponse,
Expand All @@ -25,3 +33,12 @@ export type {
CheckinResponse,
StreakRecord,
} from './interfaces/streak.interfaces';
export type {
ReferralRecord,
ReferralStatus,
ReferralSummaryResponse,
CreateReferralRequest,
QualifyReferralRequest,
PayReferralRequest,
ReferralUpdateResponse,
} from './interfaces/referral.interfaces';
107 changes: 107 additions & 0 deletions BackendAcademy/src/rewards/interfaces/referral.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Referral status options.
*
* - pending: Referee registered but has not yet met the qualifying condition
* (e.g. completing their first task / course).
* - qualified: Referee has met the qualifying condition; bonus is due.
* - paid: XLM bonus has been disbursed to the referrer's wallet.
* - expired: The referral window closed before the referee qualified.
*/
export type ReferralStatus = 'pending' | 'qualified' | 'paid' | 'expired';

/**
* A single referral record — one referrer → one referee link.
*/
export interface ReferralRecord {
/** Unique referral identifier */
id: string;
/** User ID of the person who invited the referee */
referrerId: string;
/** User ID of the person who was invited */
refereeId: string;
/** Current status of this referral */
status: ReferralStatus;
/** XLM bonus amount that will be (or has been) awarded to the referrer */
bonusAmount: number;
/** Currency of the bonus (always XLM in Phase 1) */
currency: string;
/** ISO 8601 timestamp when the referral was created */
createdAt: string;
/** ISO 8601 timestamp when the referee qualified (null if not yet qualified) */
qualifiedAt: string | null;
/** ISO 8601 timestamp when the bonus was paid out (null if not yet paid) */
paidAt: string | null;
}

/**
* Response shape for GET /rewards/referrals/:userId
*
* Returns summary stats plus a list of individual referral records
* for the given referrer.
*/
export interface ReferralSummaryResponse {
referrerId: string;
/** Total number of referrals made by this user (all statuses) */
totalReferrals: number;
/** Number of referrals that have been paid out */
paidReferrals: number;
/** Total XLM earned through referral bonuses (paid only) */
totalXlmEarned: number;
/** Pending XLM that is due once referees qualify */
pendingXlm: number;
/** All referral records for this referrer */
referrals: ReferralRecord[];
}

/**
* Request body for POST /rewards/referrals
*
* Called when a new user registers via a referral link.
*/
export interface CreateReferralRequest {
/** User ID of the referrer (the one who shared the link) */
referrerId: string;
/** User ID of the newly-registered referee */
refereeId: string;
/**
* Optional: override the default bonus amount for this referral.
* Useful for promotional campaigns. Falls back to REFERRAL_BONUS_XLM.
*/
bonusAmount?: number;
}

/**
* Request body for POST /rewards/referrals/:referralId/qualify
*
* Called by internal services when the referee completes the qualifying action
* (e.g. first task submission graded ≥ 70, first course completed, etc.).
*/
export interface QualifyReferralRequest {
/** Timestamp at which the qualifying event occurred */
qualifiedAt?: string;
}

/**
* Request body for POST /rewards/referrals/:referralId/pay
*
* Called by the payout service after the on-chain XLM transfer is confirmed.
*/
export interface PayReferralRequest {
/** ISO 8601 timestamp of the confirmed payout */
paidAt?: string;
}

/**
* Lightweight response returned after a state-changing operation
* (qualify / pay) to avoid re-fetching the full summary.
*/
export interface ReferralUpdateResponse {
referralId: string;
newStatus: ReferralStatus;
bonusAmount: number;
currency: string;
/** Set when transitioning to 'qualified' */
qualifiedAt: string | null;
/** Set when transitioning to 'paid' */
paidAt: string | null;
}
41 changes: 41 additions & 0 deletions BackendAcademy/src/rewards/referral.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Referral bonus configuration constants.
*
* All monetary values are expressed in whole XLM units.
* Replace hard-coded values with environment-variable reads
* (e.g. via ConfigService) when wiring up to production.
*/

/**
* Default XLM bonus credited to the referrer when their referee
* completes the qualifying action.
*/
export const REFERRAL_BONUS_XLM = 5;

/**
* Currency symbol for referral payouts (Phase 1 is XLM-only).
*/
export const REFERRAL_CURRENCY = 'XLM';

/**
* Number of days after which a pending referral is automatically
* marked as 'expired' if the referee has not qualified.
*
* Set to 0 to disable automatic expiry (useful for testing).
*/
export const REFERRAL_EXPIRY_DAYS = 30;

/**
* Maximum number of pending (unqualified) referrals a single user may
* hold at any one time. Prevents referral farming.
*
* Set to 0 to disable the cap.
*/
export const MAX_PENDING_REFERRALS_PER_USER = 50;

/**
* Maximum total referrals (all statuses) tracked per referrer.
* Older entries beyond this limit are not pruned automatically —
* this constant is intended for display / pagination defaults.
*/
export const REFERRAL_DISPLAY_LIMIT = 100;
Loading
Loading