loader-composition.spec.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { afterEach, describe, expect, it } from 'vitest'
  2. import { Context } from '@deepseek-ai/cordis'
  3. import Loader from '@deepseek-ai/cordis-plugin-loader'
  4. import Include from '@deepseek-ai/cordis-plugin-include'
  5. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  6. import { tmpdir } from 'node:os'
  7. import { join } from 'node:path'
  8. import { pathToFileURL } from 'node:url'
  9. import LlmRuntime, { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
  10. import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
  11. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  12. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  13. import SessionTitleService from '@deepseek-ai/dsh-session-title'
  14. import * as providerPlugin from '@deepseek-ai/dsh-session-title-first-prompt-llm'
  15. let root: string | undefined
  16. let context: Context | undefined
  17. class LoaderAdapter extends LlmAdapter {
  18. readonly requests: GenerateOptions[] = []
  19. override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
  20. this.requests.push(options)
  21. yield { type: 'text-delta', index: 0, text: 'Loader composed title' }
  22. yield { type: 'finish', reason: { kind: 'stop' } }
  23. }
  24. }
  25. afterEach(async () => {
  26. await context?.fiber.dispose()
  27. context = undefined
  28. if (root !== undefined) await rm(root, { recursive: true, force: true })
  29. root = undefined
  30. })
  31. async function loadComposition(): Promise<Context> {
  32. root = await mkdtemp(join(tmpdir(), 'dsh-title-loader-'))
  33. const configPath = join(root, 'cordis.yml')
  34. await writeFile(configPath, [
  35. "- name: '@deepseek-ai/dsh-llm'",
  36. "- name: '@deepseek-ai/dsh-session'",
  37. "- name: '@deepseek-ai/dsh-session-projection'",
  38. "- name: '@deepseek-ai/dsh-session-title'",
  39. ' config:',
  40. ' fallbackMaxWords: 5',
  41. ' fallbackMaxBytes: 40',
  42. ' maxTitleBytes: 80',
  43. "- name: '@deepseek-ai/dsh-session-title-first-prompt-llm'",
  44. ' config:',
  45. ' targetWords: 5',
  46. ' targetCjkCharacters: 10',
  47. ' maxInputBytes: 1000',
  48. ' maxOutputTokens: 32',
  49. ' timeoutMs: 1000',
  50. " provider: 'title-route'",
  51. " model: 'title-model'",
  52. '',
  53. ].join('\n'))
  54. context = new Context()
  55. context.baseUrl = pathToFileURL(root).href + '/'
  56. await context.plugin(Loader)
  57. context.loader.builtins.include = Include
  58. const modules = new Map<string, unknown>([
  59. ['@deepseek-ai/dsh-llm', LlmRuntime],
  60. ['@deepseek-ai/dsh-session', SessionStore],
  61. ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
  62. ['@deepseek-ai/dsh-session-title', SessionTitleService],
  63. ['@deepseek-ai/dsh-session-title-first-prompt-llm', providerPlugin],
  64. ])
  65. context.loader.internal = {
  66. version: 'v2',
  67. async import(specifier: string) {
  68. if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
  69. return modules.get(specifier)
  70. },
  71. } as unknown as NonNullable<typeof context.loader.internal>
  72. await context.loader.create({
  73. name: 'cordis:include',
  74. config: { path: pathToFileURL(configPath).href },
  75. })
  76. await context.loader.await()
  77. return context
  78. }
  79. describe('session-title Loader composition', () => {
  80. it('loads the service and one model provider with required deployment policy', async () => {
  81. const ctx = await loadComposition()
  82. const unloaded = [...ctx.loader.entries()]
  83. .filter(entry => entry.fiber === undefined && !entry.disabled)
  84. .map(entry => entry.options.name)
  85. expect(unloaded).toEqual([])
  86. const adapter = new LoaderAdapter()
  87. ctx.llm.registerAdapter(['title-route'], adapter)
  88. const session = ctx.sessions.create(SessionId('loader-title'))
  89. session.append('turn/start', {
  90. turn: 1,
  91. })
  92. const message = session.append('user/message', createUserMessage({
  93. content: [{ type: 'text', text: 'Compose a title through Loader' }],
  94. source: { kind: 'user' },
  95. }), { surfaceOp: 'append' })
  96. await new Promise(resolve => setTimeout(resolve, 0))
  97. session.append('request/header', {
  98. header: { config: { provider: 'main-route', model: 'main-model' } },
  99. reason: 'initial',
  100. })
  101. await new Promise(resolve => setTimeout(resolve, 0))
  102. expect(adapter.requests[0]).toMatchObject({ provider: 'title-route', model: 'title-model' })
  103. expect(ctx.sessionTitle.get(session)).toMatchObject({
  104. title: 'Loader composed title',
  105. messageSeqs: [message.seq],
  106. source: {
  107. kind: 'provider',
  108. provider: 'session-title-first-prompt-llm',
  109. model: { provider: 'title-route', model: 'title-model' },
  110. },
  111. })
  112. })
  113. })