index.ts 4.4 KB

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