@xpert-ai/plugin-rag-vlm 0.0.1 → 0.1.1

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.
@@ -1,6 +1,9 @@
1
- import { ChunkMetadata, FileSystemPermission, IImageUnderstandingStrategy, LLMPermission, TImageUnderstandingConfig, TImageUnderstandingResult } from '@xpert-ai/plugin-sdk';
2
- import { IconType, IKnowledgeDocument } from '@metad/contracts';
3
- export declare class VlmDefaultStrategy implements IImageUnderstandingStrategy {
1
+ import { FileSystemPermission, IImageUnderstandingStrategy, LLMPermission, TImageUnderstandingConfig, TImageUnderstandingResult } from '@xpert-ai/plugin-sdk';
2
+ import { IconType } from '@xpert-ai/contracts';
3
+ type VlmConfig = TImageUnderstandingConfig & {
4
+ promptTemplate?: string;
5
+ };
6
+ export declare class VlmDefaultStrategy implements IImageUnderstandingStrategy<VlmConfig> {
4
7
  readonly permissions: (FileSystemPermission | LLMPermission)[];
5
8
  readonly meta: {
6
9
  name: string;
@@ -23,7 +26,8 @@ export declare class VlmDefaultStrategy implements IImageUnderstandingStrategy {
23
26
  };
24
27
  };
25
28
  validateConfig(config: TImageUnderstandingConfig): Promise<void>;
26
- understandImages(doc: IKnowledgeDocument<ChunkMetadata>, config: TImageUnderstandingConfig): Promise<TImageUnderstandingResult>;
29
+ understandImages(doc: Parameters<IImageUnderstandingStrategy['understandImages']>[0], config: VlmConfig): Promise<TImageUnderstandingResult>;
27
30
  private runV;
28
31
  }
32
+ export {};
29
33
  //# sourceMappingURL=vlm.strategy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vlm.strategy.d.ts","sourceRoot":"","sources":["../../src/lib/vlm.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAE3B,aAAa,EACb,yBAAyB,EACzB,yBAAyB,EAC1B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAqC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AASlG,qBAEa,kBAAmB,YAAW,2BAA2B;IACpE,QAAQ,CAAC,WAAW,2CAUnB;IAED,QAAQ,CAAC,IAAI;;;;;;;;;;;;;;;kBAYM,QAAQ;;;;MAI1B;IAEK,cAAc,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhE,gBAAgB,CACpB,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,EACtC,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,yBAAyB,CAAC;YAoDvB,IAAI;CA4BnB"}
1
+ {"version":3,"file":"vlm.strategy.d.ts","sourceRoot":"","sources":["../../src/lib/vlm.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,oBAAoB,EACpB,2BAA2B,EAE3B,aAAa,EACb,yBAAyB,EAEzB,yBAAyB,EAC1B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAqC,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AASjF,KAAK,SAAS,GAAG,yBAAyB,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAExE,qBAEa,kBAAmB,YAAW,2BAA2B,CAAC,SAAS,CAAC;IAC/E,QAAQ,CAAC,WAAW,2CAUnB;IAED,QAAQ,CAAC,IAAI;;;;;;;;;;;;;;;kBAYM,QAAQ;;;;MAI1B;IAEK,cAAc,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhE,gBAAgB,CACpB,GAAG,EAAE,UAAU,CAAC,2BAA2B,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,EACnE,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,yBAAyB,CAAC;YA0DvB,IAAI;CAqDnB"}
@@ -1,7 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { Injectable } from '@nestjs/common';
3
3
  import { ImageUnderstandingStrategy } from '@xpert-ai/plugin-sdk';
4
- import { buildChunkTree, collectTreeLeaves } from '@metad/contracts';
4
+ import { buildChunkTree, collectTreeLeaves } from '@xpert-ai/contracts';
5
5
  import { Document } from '@langchain/core/documents';
6
6
  import sharp from 'sharp';
7
7
  import { v4 as uuid } from 'uuid';
@@ -18,7 +18,7 @@ let VlmDefaultStrategy = class VlmDefaultStrategy {
18
18
  },
19
19
  {
20
20
  type: 'llm',
21
- capability: 'vision',
21
+ capability: 'vision'
22
22
  }
23
23
  ];
24
24
  this.meta = {
@@ -47,71 +47,101 @@ let VlmDefaultStrategy = class VlmDefaultStrategy {
47
47
  async understandImages(doc, config) {
48
48
  await this.validateConfig(config);
49
49
  const client = config.visionModel; // ✅ Injected by the core system
50
- const params = {
51
- files: doc.metadata?.assets?.filter((asset) => asset.type === 'image'),
52
- chunks: doc.chunks
53
- };
54
- const tree = buildChunkTree(doc.chunks);
55
- const leaves = collectTreeLeaves(tree);
50
+ const files = doc.metadata?.assets?.filter((asset) => asset.type === 'image') ?? [];
51
+ const leaves = collectTreeLeaves(buildChunkTree(doc.chunks));
52
+ // Keep context parents as well as retrieval leaves, and avoid repeated OCR for overlapping chunks.
56
53
  const chunks = [];
57
- // const pages : Document<Partial<ChunkMetadata>>[] = []
58
- for await (const chunk of leaves) {
59
- const assets = [];
60
- chunk.metadata['chunkId'] ??= uuid();
61
- // Source Document Block
54
+ const leafIds = new Set(leaves.map((chunk) => chunk.metadata.chunkId));
55
+ chunks.push(...doc.chunks.filter((chunk) => !leafIds.has(chunk.metadata.chunkId)));
56
+ const processed = new Set();
57
+ const warnings = [];
58
+ for (const chunk of leaves) {
62
59
  chunks.push(chunk);
63
- // Find image tags inside the chunk
64
- const matches = Array.from(chunk.pageContent.matchAll(IMAGE_REGEX));
65
- for (const match of matches) {
66
- const url = match[1]; // image-url.png
67
- const asset = params.files.find((a) => a.url === url);
68
- if (asset && !assets.some((_) => _ === asset.url)) {
69
- const description = await this.runV(client, chunk.pageContent, asset.filePath, config);
70
- assets.push(asset.url);
60
+ for (const match of chunk.pageContent.matchAll(IMAGE_REGEX)) {
61
+ const asset = files.find((item) => item.url === match[1]);
62
+ if (!asset || processed.has(asset.filePath))
63
+ continue;
64
+ processed.add(asset.filePath);
65
+ try {
66
+ const result = await this.runV(client, asset, config, chunk.pageContent);
67
+ if (result.type === 'skipped') {
68
+ warnings.push({ type: 'image_understanding_skipped', message: result.reason, imagePath: asset.filePath });
69
+ continue;
70
+ }
71
+ const description = result.text;
72
+ if (!description.trim())
73
+ throw new Error('The vision model returned no text.');
71
74
  chunks.push(new Document({
72
75
  pageContent: description,
73
76
  metadata: {
74
77
  mediaType: 'image',
75
- chunkId: `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
76
- parentId: chunk.metadata['chunkId'],
78
+ chunkId: uuid(),
79
+ // Page transcriptions are independent source text and will be split by the host.
80
+ ...(asset.sourceType === 'pdf_page'
81
+ ? { page: asset.page, sourceType: 'pdf_page', contentFormat: 'markdown' }
82
+ : { parentId: chunk.metadata.chunkId }),
77
83
  imagePath: asset.filePath,
78
- // source: asset.filename,
84
+ imageUrl: asset.url,
79
85
  parser: 'vlm'
80
86
  }
81
87
  }));
82
88
  }
89
+ catch (error) {
90
+ warnings.push({
91
+ type: 'image_understanding_failed',
92
+ message: error instanceof Error ? error.message : 'Image recognition failed.',
93
+ imagePath: asset.filePath
94
+ });
95
+ }
83
96
  }
84
97
  }
85
- return {
86
- chunks,
87
- metadata: {}
88
- };
98
+ return { chunks, metadata: { warnings } };
89
99
  }
90
- async runV(client, context, imagePath, config) {
91
- const imageStr = await config.permissions.fileSystem.readFile(imagePath);
92
- const sharped = sharp(imageStr);
93
- const imageData = await sharped.resize(1024).toBuffer();
94
- const fileInfo = await sharped.metadata();
95
- const mimetype = fileInfo.format ? `image/${fileInfo.format}` : 'image/png';
100
+ async runV(client, asset, config, context) {
101
+ const imageStr = await config.permissions.fileSystem.readFile(asset.filePath);
102
+ const page = asset.sourceType === 'pdf_page';
103
+ const image = sharp(imageStr);
104
+ const { width, height } = await image.metadata();
105
+ // Embedded tracking/spacer pixels contain no readable content. Never silently skip a PDF page.
106
+ if (width === 1 || height === 1) {
107
+ if (page)
108
+ throw new Error(`PDF page image is too small to recognize (${width}×${height}).`);
109
+ return { type: 'skipped', reason: `Skipped a ${width}×${height} placeholder image.` };
110
+ }
111
+ const imageData = await image
112
+ .resize({ width: page ? 2200 : 1024, height: page ? 2200 : 1024, fit: 'inside', withoutEnlargement: true })
113
+ .png()
114
+ .toBuffer();
96
115
  const response = await client.invoke([
97
116
  {
98
117
  role: 'system',
99
- content: 'You are a professional assistant, helping people understand images in context. Please provide a narrative description of the image.'
118
+ content: page
119
+ ? 'Transcribe all visible content of this document page into Markdown in reading order. Preserve headings, lists, table rows and merged-cell relationships. Copy numbers, leading zeroes, codes and punctuation exactly. Do not summarize, describe the page, invent text, or add a preface. Mark unreadable text as [unreadable].'
120
+ : 'You are a professional assistant, helping people understand images in context. Please provide a narrative description of the image.'
100
121
  },
101
122
  {
102
123
  role: 'user',
103
124
  content: [
104
- // { type: 'text', text: context },
125
+ ...(config.promptTemplate?.trim()
126
+ ? [{ type: 'text', text: config.promptTemplate.replaceAll('{{context}}', () => context) }]
127
+ : []),
105
128
  {
106
129
  type: 'image_url',
107
130
  image_url: {
108
- url: `data:${mimetype};base64,${imageData.toString('base64')}`
131
+ url: `data:image/png;base64,${imageData.toString('base64')}`
109
132
  }
110
133
  }
111
134
  ]
112
135
  }
113
136
  ]);
114
- return response.content;
137
+ return {
138
+ type: 'recognized',
139
+ text: typeof response.content === 'string'
140
+ ? response.content
141
+ : response.content
142
+ .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
143
+ .join('\n')
144
+ };
115
145
  }
116
146
  };
117
147
  VlmDefaultStrategy = __decorate([
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@xpert-ai/plugin-rag-vlm",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/xpert-ai/xpert-plugins"
7
+ },
4
8
  "type": "module",
5
9
  "main": "./dist/index.js",
6
10
  "module": "./dist/index.js",
@@ -23,13 +27,13 @@
23
27
  "tslib": "^2.3.0"
24
28
  },
25
29
  "peerDependencies": {
26
- "@xpert-ai/plugin-sdk": "^3.6.0",
30
+ "@xpert-ai/plugin-sdk": "^3.15.18",
27
31
  "zod": "3.25.67",
28
32
  "@nestjs/config": "^4.0.2",
29
33
  "chalk": "4.1.2",
30
34
  "@langchain/core": "0.3.72",
31
35
  "@nestjs/common": "^11.1.6",
32
- "@metad/contracts": "^3.6.0",
36
+ "@xpert-ai/contracts": "^3.6.0",
33
37
  "uuid": "8.3.2"
34
38
  }
35
39
  }