Skip to content

feat(assistant): POST /assistant/naming — AI-generated event title & description candidates #49 - #79

Merged
Olowodarey merged 1 commit into
Arena1X:mainfrom
Awointa:feature/assistant-naming
Jul 30, 2026
Merged

feat(assistant): POST /assistant/naming — AI-generated event title & description candidates #49#79
Olowodarey merged 1 commit into
Arena1X:mainfrom
Awointa:feature/assistant-naming

Conversation

@Awointa

@Awointa Awointa commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

closes #49

Summary

Resolves the blank-title UX problem for event creators. Given a free-text
fixture slate summary, the endpoint generates 3 catchy title candidates (≤ 40
chars each) and one event description (≤ 200 chars) via the shared LlmService.
Results are cached for 24 hours so identical slates cost zero additional LLM
calls.

Changes

New endpoint

  • POST /api/v1/assistant/naming — accepts slateSummary (required, ≤ 500 chars)
    and an optional context hint (tone/audience). Returns titles, description,
    fallbackUsed, and cachedAt.

Validation (truncation strategy)

  • Candidates exceeding the length limit are truncated at the nearest word
    boundary then re-checked for blocked terms. Truncation is chosen over
    regeneration because it is deterministic and avoids unbounded LLM round-trips.
    This decision is documented in the service JSDoc.
  • A shared PROFANITY_BLOCKLIST constant is introduced in src/common/constants/
    with a containsBlockedTerm() helper. Matching uses a word-start anchor
    (\bterm) so root forms catch common derivations ("fuck" catches "fucking",
    "fucked", etc.).
  • Any candidate that remains blocked after truncation is replaced by a
    deterministic fallback.

Caching

  • In-memory Map keyed by SHA-256(slateSummary || context), 24-hour TTL. No new
    dependencies. Cache resets on restart, which is acceptable for this use case.

Fallback

  • Triggered when the LLM is unconfigured, throws, or returns unparseable
    output.
  • Fallback titles follow the pattern "Premier League Showdown — Week of
    {date}" where the date label is extracted from the summary or defaults to
    today's date.
  • fallbackUsed: true is always set so callers can detect and surface the
    fallback if needed.

Files changed

  • src/common/constants/profanity-blocklist.ts — new shared blocklist constant
    and helper
  • src/assistant/dto/naming.dto.ts — request and response DTOs
  • src/assistant/prompts/naming.prompt.ts — LLM prompt builder
  • src/assistant/naming.service.ts — core service (LLM, cache, validation,
    fallback)
  • src/assistant/naming.service.spec.ts — 19 unit tests
  • src/assistant/assistant.controller.ts — new route wired to NamingService
  • src/assistant/assistant.module.ts — NamingService registered as provider

Tests

19 unit tests covering all acceptance criteria:

  • Length enforcement — truncation brings all titles to ≤ 40 chars and
    descriptions to ≤ 200 chars; exact-40-char titles are preserved untouched
  • Blocklist — blocked titles are replaced by fallbacks; blocked descriptions
    fall back to a safe generic string; clean output passes through unchanged
  • Cache hit — identical requests call the LLM exactly once; different
    summaries, different contexts, and expired entries each trigger a fresh LLM
    call
  • Fallback — triggers on unconfigured LLM, thrown error, non-JSON output, and
    missing fields; fallback output always satisfies length and blocklist
    constraints

Tested

  • pnpm run build — clean TypeScript compile, no errors
  • Full test suite — 119/119 passing across all spec files, no regressions

- NamingRequestDto: slateSummary (≤500) + optional context (≤200)
- NamingResponseDto: 3 title candidates (≤40 chars), one description
  (≤200 chars), fallbackUsed flag, cachedAt timestamp
- NamingService: LLM-backed via LlmService.complete(), in-process Map
  cache keyed by SHA-256(slateSummary||context), 24h TTL
- Validation strategy: truncate at nearest word boundary then reject on
  blocked term → replace with fallback title/description
- PROFANITY_BLOCKLIST constant in src/common/constants with
  prefix-match helper (\bterm catches derivations like 'fucking')
- Deterministic fallback: 'Premier League Showdown — Week of {date}'
  pattern, date label extracted from summary or today's date
- AssistantController: new POST /assistant/naming route
- AssistantModule: NamingService registered as provider
- 19 unit tests covering: length enforcement (truncation), blocklist,
  cache hit / miss / expiry / key isolation, fallback (unconfigured
  LLM, throws, non-JSON, missing fields, always-valid guarantee)
@Olowodarey
Olowodarey merged commit 2c2ac60 into Arena1X:main Jul 30, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[AI-Agent] — Event Title and Description Generator

2 participants