index.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * Session feedback: the `feedback/record` event, its command-independent
  3. * producer, the `sessionFeedback` Host Remote a product surface records
  4. * through, and the human-facing `/feedback` command. Recording appends one
  5. * authoritative log-only event and does not start model work. The append is
  6. * eager but unflushed, so acknowledgement reports that the entry is logged,
  7. * not that it reached disk.
  8. * @module @deepseek-ai/dsh-command-feedback
  9. */
  10. import type { Context } from '@deepseek-ai/cordis'
  11. import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
  12. import type { Session } from '@deepseek-ai/dsh-session'
  13. import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
  14. import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
  15. import type {
  16. FeedbackCategory,
  17. FeedbackRecord,
  18. SessionFeedbackRecordRequest,
  19. SessionFeedbackRecordResult,
  20. } from './types.ts'
  21. export type * from './types.ts'
  22. /**
  23. * Every feedback category in the order product surfaces present them; each
  24. * surface owns its localized labels.
  25. */
  26. export const FEEDBACK_CATEGORIES = [
  27. 'task-result',
  28. 'instruction-following',
  29. 'product-interaction',
  30. 'service-stability',
  31. 'resource-cost',
  32. 'security-privacy-permission',
  33. 'other',
  34. ] as const satisfies readonly FeedbackCategory[]
  35. export const name = 'command-feedback'
  36. export const inject = ['commands']
  37. const USAGE = 'Usage: /feedback <text>'
  38. declare module '@deepseek-ai/cordis' {
  39. interface Context {
  40. sessionFeedback: SessionFeedbackService
  41. }
  42. }
  43. /**
  44. * Record feedback independently of any UI trigger. Surrounding whitespace is
  45. * discarded and a blank text is recorded as absent; an entry with neither
  46. * text nor category is still recorded.
  47. * @param session - session the feedback describes.
  48. * @param entry - human-authored remark and its category.
  49. */
  50. export function recordFeedback(session: Session, entry: FeedbackRecord): void {
  51. const text = entry.text?.trim() ?? ''
  52. session.append('feedback/record', {
  53. ...(text.length === 0 ? {} : { text }),
  54. ...(entry.category === undefined ? {} : { category: entry.category }),
  55. })
  56. }
  57. /**
  58. * Validate, record, and acknowledge one feedback entry. Returning an error
  59. * leaves no `feedback/record` event.
  60. * @param invocation - receiving agent, raw command input, and UI cancellation.
  61. * @returns an acknowledgement containing the receiving session and anonymous
  62. * user ids, or a usage error when no feedback text was supplied.
  63. */
  64. function executeFeedbackCommand(invocation: CommandInvocation): CommandResult {
  65. if (invocation.rawInput.trim().length === 0) {
  66. return { kind: 'error', text: `Feedback text is required. ${USAGE}` }
  67. }
  68. recordFeedback(invocation.agent.session, { text: invocation.rawInput })
  69. return {
  70. kind: 'success',
  71. text: `Feedback recorded for session ${invocation.agent.session.id}\nAnonymous user: ${getOrCreateAnonymousUserId()}.`,
  72. }
  73. }
  74. /** Host Remote through which a product surface records a Session-level remark. */
  75. export class SessionFeedbackService extends TypertRemoteService {
  76. static inject = ['sessions']
  77. /**
  78. * @param ctx - Host context carrying the live Session store.
  79. */
  80. constructor(ctx: Context) {
  81. super(ctx, 'sessionFeedback')
  82. }
  83. /**
  84. * Record one remark on a live Session.
  85. * @param request - target Session plus the optional text and category.
  86. * @returns the recorded postcondition, or `session-not-found` when no live
  87. * Session carries the id.
  88. */
  89. @Remote('record')
  90. record(request: SessionFeedbackRecordRequest): Promise<SessionFeedbackRecordResult> {
  91. const session = this.ctx.sessions.get(request.sessionId)
  92. if (session === undefined) {
  93. return Promise.resolve({ ok: false, error: { code: 'session-not-found', sessionId: request.sessionId } })
  94. }
  95. recordFeedback(session, request)
  96. return Promise.resolve({ ok: true, value: { recorded: true } })
  97. }
  98. }
  99. /**
  100. * Register the global `/feedback` command for every composed command adapter
  101. * and mount the `sessionFeedback` Remote.
  102. * @param ctx - Host context.
  103. */
  104. export function apply(ctx: Context): void {
  105. ctx.plugin(SessionFeedbackService)
  106. ctx.commands.register({
  107. name: 'feedback',
  108. description: 'record feedback about this session',
  109. input: { hint: '<text>' },
  110. recordInput: false,
  111. handler: executeFeedbackCommand,
  112. })
  113. }