ctod 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { Translator, TranslatorParams } from '../core/translator.js'
5
5
  import { ValidateCallback, ValidateCallbackOutputs, validateToJsonSchema } from '../utils/validate.js'
6
6
  import { ParserError } from '../utils/error.js'
7
7
  import { z } from 'zod'
8
+ import { CtoD } from '../ctod.js'
8
9
 
9
10
  export type PolymorphicMessage = {
10
11
  type: 'text' | 'image'
@@ -223,6 +224,15 @@ export class ChatBroker<
223
224
  }
224
225
  }
225
226
 
227
+ cloneFrom(ctod: CtoD<any, any>) {
228
+ const newParams = structuredClone(this.params)
229
+ newParams.request = ctod.params.request
230
+ if (ctod.params.plugins) {
231
+ newParams.plugins = ctod.params.plugins
232
+ }
233
+ return new ChatBroker(newParams)
234
+ }
235
+
226
236
  requestWithId<T extends Translator<S, O>>(data: T['__schemeType']): {
227
237
  id: string
228
238
  request: Promise<T['__outputType']>
@@ -6,11 +6,18 @@ import { PolymorphicMessage } from '../../broker/chat.js'
6
6
 
7
7
  type Message = {
8
8
  role: string
9
- content: string
10
- contents: PolymorphicMessage[]
9
+ content?: string
10
+ contents?: PolymorphicMessage[]
11
11
  }
12
12
 
13
13
  type Options = any
14
+ type Content = {
15
+ type: 'text' | 'image_url'
16
+ text?: string
17
+ image_url?: {
18
+ url: string
19
+ }
20
+ }[]
14
21
 
15
22
  export type Config = {
16
23
  baseUrl: string
@@ -201,7 +208,22 @@ export class LlamaCppCompletion {
201
208
  const template = new Template(props.chat_template)
202
209
  const prompt = template.render({
203
210
  bos_token: props.bos_token,
204
- messages: params.messages
211
+ messages: params.messages.map(e => {
212
+ const output: any = { role: e.role, content: [] as Content }
213
+ if (e.content) {
214
+ output.content.push({ type: 'text', text: this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content })
215
+ }
216
+ if (e.contents) {
217
+ e.contents.forEach(item => {
218
+ if (item.type === 'text') {
219
+ output.content.push({ type: 'text', text: this.config.autoConvertTraditionalChinese ? t2s(item.content) : item.content })
220
+ } else if (item.type === 'image') {
221
+ output.content.push({ type: 'image_url', image_url: { url: item.content } })
222
+ }
223
+ })
224
+ }
225
+ return output
226
+ })
205
227
  }).slice(0, props.eos_token.length * -1 - 1)
206
228
  const result = await requester.fetch({
207
229
  path: 'completion',
@@ -244,7 +266,22 @@ export class LlamaCppCompletion {
244
266
  const template = new Template(props.chat_template)
245
267
  const prompt = template.render({
246
268
  bos_token: props.bos_token,
247
- messages: params.messages
269
+ messages: params.messages.map(e => {
270
+ const output: any = { role: e.role, content: [] as Content }
271
+ if (e.content) {
272
+ output.content.push({ type: 'text', text: this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content })
273
+ }
274
+ if (e.contents) {
275
+ e.contents.forEach(item => {
276
+ if (item.type === 'text') {
277
+ output.content.push({ type: 'text', text: this.config.autoConvertTraditionalChinese ? t2s(item.content) : item.content })
278
+ } else if (item.type === 'image') {
279
+ output.content.push({ type: 'image_url', image_url: { url: item.content } })
280
+ }
281
+ })
282
+ }
283
+ return output
284
+ })
248
285
  }).slice(0, props.eos_token.length * -1 - 1)
249
286
  return {
250
287
  ...(params.options || {}),
@@ -278,18 +315,30 @@ export class LlamaCppCompletion {
278
315
  messages: params.messages.map(e => {
279
316
  const output = {
280
317
  role: e.role,
281
- content: ''
318
+ content: [] as Content
282
319
  }
283
320
  if (e.content) {
284
- output.content = this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content
321
+ output.content.push({
322
+ type: 'text',
323
+ text: this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content
324
+ })
285
325
  }
286
326
  if (e.contents) {
287
- output.content += e.contents.map(item => {
327
+ e.contents.forEach(item => {
288
328
  if (item.type === 'text') {
289
- return item.content
329
+ output.content.push({
330
+ type: 'text',
331
+ text: this.config.autoConvertTraditionalChinese ? t2s(item.content) : item.content
332
+ })
333
+ } else if (item.type === 'image') {
334
+ output.content.push({
335
+ type: 'image_url',
336
+ image_url: {
337
+ url: item.content
338
+ }
339
+ })
290
340
  }
291
- return ''
292
- }).join('\n')
341
+ })
293
342
  }
294
343
  return output
295
344
  })
@@ -330,10 +379,34 @@ export class LlamaCppCompletion {
330
379
  ...(params.options || {}),
331
380
  stream: true,
332
381
  messages: params.messages.map(e => {
333
- return {
382
+ const output = {
334
383
  role: e.role,
335
- content: this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content
384
+ content: [] as Content
385
+ }
386
+ if (e.content) {
387
+ output.content.push({
388
+ type: 'text',
389
+ text: this.config.autoConvertTraditionalChinese ? t2s(e.content) : e.content
390
+ })
391
+ }
392
+ if (e.contents) {
393
+ e.contents.forEach(item => {
394
+ if (item.type === 'text') {
395
+ output.content.push({
396
+ type: 'text',
397
+ text: this.config.autoConvertTraditionalChinese ? t2s(item.content) : item.content
398
+ })
399
+ } else if (item.type === 'image') {
400
+ output.content.push({
401
+ type: 'image_url',
402
+ image_url: {
403
+ url: item.content
404
+ }
405
+ })
406
+ }
407
+ })
336
408
  }
409
+ return output
337
410
  })
338
411
  }
339
412
  })
@@ -33,7 +33,8 @@ export class LlamaCppCtodService {
33
33
  })
34
34
  onCancel(cancel)
35
35
  const { message } = await run()
36
- return chat.config.autoConvertTraditionalChinese ? s2t(message) : message
36
+ const stringMessage = typeof message === 'string' ? message : JSON.stringify(message)
37
+ return chat.config.autoConvertTraditionalChinese ? s2t(stringMessage) : stringMessage
37
38
  }
38
39
  }
39
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctod",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "CtoD Is Chat To Data Utils.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -62,6 +62,6 @@
62
62
  "json5": "^2.2.3",
63
63
  "opencc-js": "^1.0.5",
64
64
  "power-helper": "^0.9.0",
65
- "zod": "^4.1.12"
65
+ "zod": "^4.3.6"
66
66
  }
67
67
  }
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: ctod
3
+ description: Use this skill whenever writing or generating BrokerBuilder and Broker code with CtoD. Trigger this skill whenever the user asks to add or modify a Broker, or define LLM input/output structures.
4
+ ---
5
+
6
+ # CtoD — BrokerBuilder & Broker Generation Guide
7
+
8
+ ## Step 1: Create a BrokerBuilder
9
+
10
+ Define the input type and shared system prompt.
11
+
12
+ ```ts
13
+ // This is just for reference. In best practice, the ctod instance should already be initialized
14
+ // in a shared place — no need to recreate it in every skill.
15
+ import { CtoD } from '../lib/index.js'
16
+ const ctod = new CtoD({ ... })
17
+
18
+ const brokerBuilder = ctod.createBrokerBuilder<{
19
+ question: string // input type
20
+ }>({
21
+ install: ({ attach }) => {
22
+ attach('start', async ({ setPreMessages }) => {
23
+ setPreMessages([
24
+ { role: 'system', content: 'You are a helpful assistant.' }
25
+ ])
26
+ })
27
+ }
28
+ })
29
+ ```
30
+
31
+ ## Step 2: Create a Broker
32
+
33
+ ```ts
34
+ const broker = brokerBuilder.create(async ({ zod, data, setMessages }) => {
35
+ setMessages([
36
+ {
37
+ role: 'user',
38
+ contents: [{ type: 'text', content: `Question: ${data.question}` }]
39
+ }
40
+ ])
41
+ return {
42
+ answer: zod.string().describe('The answer'),
43
+ confidence: zod.number().min(0).max(1).describe('Confidence score')
44
+ }
45
+ })
46
+
47
+ const result = await broker.request({ question: '...' })
48
+ ```
49
+
50
+ ## Message Formats
51
+
52
+ ```ts
53
+ // Plain text (shorthand)
54
+ { role: 'user', content: 'Hello' }
55
+
56
+ // Multi-content (with image)
57
+ { role: 'user', contents: [
58
+ { type: 'text', content: 'What is in this image?' },
59
+ { type: 'image', content: 'data:image/png;base64,...' }
60
+ ]}
61
+
62
+ // Multi-line text composition
63
+ import { paragraph } from 'ctod'
64
+ paragraph(['First paragraph', JSON.stringify(data), `Question: ${question}`])
65
+ ```
66
+
67
+ ## Output Schema Patterns
68
+
69
+ ```ts
70
+ // Single value
71
+ { answer: zod.string().describe('...') }
72
+
73
+ // Array
74
+ {
75
+ items: zod.array(
76
+ zod.object({
77
+ name: zod.string().describe('Name'),
78
+ score: zod.number().describe('Score from 0 to 1')
79
+ })
80
+ ).describe('Sorted by relevance')
81
+ }
82
+ ```
83
+
84
+ ## Hooks (used inside BrokerBuilder install)
85
+
86
+ | Hook | Triggered when | Common params |
87
+ |---|---|---|
88
+ | `start` | Request starts | `setPreMessages`, `changeMessages`, `data`, `metadata` |
89
+ | `talkBefore` | Before sending | `messages`, `lastUserMessage` |
90
+ | `talkAfter` | After LLM responds | `changeParseText`, `parseFail` |
91
+ | `succeeded` | Parse succeeded | `output`, `metadata` |
92
+ | `parseFailed` | Parse failed | `retry`, `changeMessages` |
93
+ | `done` | Request ends | `id`, `metadata` |
94
+
95
+ ```ts
96
+ // metadata shares data across hooks
97
+ attach('start', async ({ metadata }) => { metadata.set('key', value) })
98
+ attach('done', async ({ metadata }) => { metadata.get('key') })
99
+ ```
100
+
101
+ ## Notes
102
+
103
+ - `zod` is injected by ctod — **no need to import it separately**
104
+ - `setPreMessages` can only be used in the `start` hook
105
+ - `changeMessages` can be used in multiple hooks
@@ -3,6 +3,7 @@ import { Event, Hook, Log } from 'power-helper';
3
3
  import { Translator, TranslatorParams } from '../core/translator.js';
4
4
  import { ValidateCallback, ValidateCallbackOutputs } from '../utils/validate.js';
5
5
  import { z } from 'zod';
6
+ import { CtoD } from '../ctod.js';
6
7
  export type PolymorphicMessage = {
7
8
  type: 'text' | 'image';
8
9
  content: string;
@@ -147,6 +148,7 @@ export declare class ChatBroker<S extends ValidateCallback<any>, O extends Valid
147
148
  constructor(params: Params<S, O, C, P, PS>);
148
149
  protected _install(): any;
149
150
  cancel(requestId?: string): Promise<void>;
151
+ cloneFrom(ctod: CtoD<any, any>): ChatBroker<S, O, P, PS, C>;
150
152
  requestWithId<T extends Translator<S, O>>(data: T['__schemeType']): {
151
153
  id: string;
152
154
  request: Promise<T['__outputType']>;
@@ -164,7 +166,9 @@ export declare class ChatBroker<S extends ValidateCallback<any>, O extends Valid
164
166
  outputSchema: {
165
167
  [x: string]: any;
166
168
  };
167
- outputJsonSchema: z.core.JSONSchema.JSONSchema;
169
+ outputJsonSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
170
+ [x: string]: any;
171
+ }, z.core.$strip>>;
168
172
  requestMessages: Message[];
169
173
  }>;
170
174
  }
@@ -2,8 +2,8 @@ import { LlamaCppCtodService } from './index.js';
2
2
  import { PolymorphicMessage } from '../../broker/chat.js';
3
3
  type Message = {
4
4
  role: string;
5
- content: string;
6
- contents: PolymorphicMessage[];
5
+ content?: string;
6
+ contents?: PolymorphicMessage[];
7
7
  };
8
8
  type Options = any;
9
9
  export type Config = {
@@ -7,4 +7,4 @@ export type ValidateCallbackOutputs<T extends ValidateCallback<any>, R = ReturnT
7
7
  };
8
8
  export declare function definedValidateSchema<T extends ValidateCallback<any>>(cb: T): T;
9
9
  export declare function validate<T extends ValidateCallback<any>, R = ReturnType<T>>(target: any, schemaCallback: T): { [K in keyof R]: R[K] extends z.ZodTypeAny ? z.infer<R[K]> : R[K]; };
10
- export declare function validateToJsonSchema<T extends ValidateCallback<any>>(target: () => T): z.core.JSONSchema.JSONSchema;
10
+ export declare function validateToJsonSchema<T extends ValidateCallback<any>>(target: () => T): z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{ -readonly [P in keyof T]: T[P]; }, z.core.$strip>>;