@lobehub/chat 1.136.12 → 1.136.13
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.
- package/.github/workflows/claude-translator.yml +13 -1
- package/CHANGELOG.md +34 -0
- package/changelog/v1.json +12 -0
- package/locales/ar/modelProvider.json +12 -0
- package/locales/ar/models.json +39 -24
- package/locales/bg-BG/modelProvider.json +12 -0
- package/locales/bg-BG/models.json +39 -24
- package/locales/de-DE/modelProvider.json +12 -0
- package/locales/de-DE/models.json +39 -24
- package/locales/en-US/modelProvider.json +12 -0
- package/locales/en-US/models.json +39 -24
- package/locales/es-ES/modelProvider.json +12 -0
- package/locales/es-ES/models.json +39 -24
- package/locales/fa-IR/modelProvider.json +12 -0
- package/locales/fa-IR/models.json +39 -24
- package/locales/fr-FR/modelProvider.json +12 -0
- package/locales/fr-FR/models.json +39 -24
- package/locales/it-IT/modelProvider.json +12 -0
- package/locales/it-IT/models.json +39 -24
- package/locales/ja-JP/modelProvider.json +12 -0
- package/locales/ja-JP/models.json +39 -24
- package/locales/ko-KR/modelProvider.json +12 -0
- package/locales/ko-KR/models.json +39 -24
- package/locales/nl-NL/modelProvider.json +12 -0
- package/locales/nl-NL/models.json +39 -24
- package/locales/pl-PL/modelProvider.json +12 -0
- package/locales/pl-PL/models.json +39 -24
- package/locales/pt-BR/modelProvider.json +12 -0
- package/locales/pt-BR/models.json +39 -24
- package/locales/ru-RU/modelProvider.json +12 -0
- package/locales/ru-RU/models.json +39 -24
- package/locales/tr-TR/modelProvider.json +12 -0
- package/locales/tr-TR/models.json +39 -24
- package/locales/vi-VN/modelProvider.json +12 -0
- package/locales/vi-VN/models.json +39 -24
- package/locales/zh-CN/modelProvider.json +12 -0
- package/locales/zh-CN/models.json +39 -24
- package/locales/zh-TW/modelProvider.json +12 -0
- package/locales/zh-TW/models.json +39 -24
- package/package.json +3 -3
- package/packages/const/src/settings/index.ts +1 -0
- package/packages/database/package.json +7 -5
- package/packages/electron-client-ipc/src/events/index.ts +2 -2
- package/packages/electron-client-ipc/src/events/{localFile.ts → localSystem.ts} +25 -6
- package/packages/electron-client-ipc/src/types/index.ts +1 -1
- package/packages/electron-client-ipc/src/types/{localFile.ts → localSystem.ts} +89 -4
- package/packages/file-loaders/package.json +1 -2
- package/packages/file-loaders/src/loadFile.ts +4 -1
- package/packages/file-loaders/src/loaders/doc/__snapshots__/index.test.ts.snap +46 -0
- package/packages/file-loaders/src/loaders/doc/index.test.ts +38 -0
- package/packages/file-loaders/src/loaders/doc/index.ts +57 -0
- package/packages/file-loaders/src/loaders/docx/index.ts +36 -45
- package/packages/file-loaders/src/loaders/index.ts +2 -0
- package/packages/file-loaders/src/types/word-extractor.d.ts +9 -0
- package/packages/file-loaders/src/types.ts +1 -1
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +267 -38
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +45 -0
- package/packages/model-runtime/src/providerTestUtils.ts +0 -5
- package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +57 -44
- package/packages/model-runtime/src/providers/anthropic/generateObject.ts +28 -20
- package/packages/model-runtime/src/providers/deepseek/index.ts +5 -0
- package/packages/model-runtime/src/providers/openai/index.test.ts +0 -5
- package/packages/model-runtime/src/providers/openrouter/index.test.ts +3 -3
- package/packages/model-runtime/src/providers/openrouter/index.ts +32 -20
- package/packages/model-runtime/src/providers/openrouter/type.ts +25 -24
- package/packages/model-runtime/src/providers/zhipu/index.test.ts +0 -1
- package/packages/model-runtime/src/types/structureOutput.ts +13 -1
- package/packages/model-runtime/src/utils/handleOpenAIError.test.ts +0 -5
- package/packages/model-runtime/src/utils/handleOpenAIError.ts +2 -2
- package/packages/types/src/aiChat.ts +13 -1
- package/packages/types/src/index.ts +1 -0
- package/src/features/ChatInput/InputEditor/index.tsx +39 -26
- package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +1 -1
- package/src/server/routers/lambda/agent.ts +2 -3
- package/src/server/routers/lambda/aiChat.ts +33 -1
- package/src/server/routers/lambda/chunk.ts +2 -2
- package/src/services/electron/file.ts +1 -2
- package/src/services/electron/localFileService.ts +40 -0
- package/src/tools/local-system/Placeholder/ListFiles.tsx +23 -0
- package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +9 -0
- package/src/tools/local-system/Placeholder/SearchFiles.tsx +55 -0
- package/src/tools/local-system/Placeholder/index.tsx +25 -0
- package/src/tools/placeholders.ts +3 -0
|
@@ -1,70 +1,61 @@
|
|
|
1
|
-
import { DocxLoader as LangchainDocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
|
2
1
|
import debug from 'debug';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import mammoth from 'mammoth';
|
|
3
4
|
|
|
4
5
|
import type { DocumentPage, FileLoaderInterface } from '../../types';
|
|
5
6
|
|
|
6
7
|
const log = debug('file-loaders:docx');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Loads Word documents (.docx) using
|
|
10
|
+
* Loads Word documents (.docx) using mammoth library.
|
|
11
|
+
* Extracts text content and basic metadata from DOCX files.
|
|
10
12
|
*/
|
|
11
13
|
export class DocxLoader implements FileLoaderInterface {
|
|
12
14
|
async loadPages(filePath: string): Promise<DocumentPage[]> {
|
|
13
15
|
log('Loading DOCX file:', filePath);
|
|
14
16
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} else {
|
|
19
|
-
loader = new LangchainDocxLoader(filePath, { type: 'docx' });
|
|
20
|
-
}
|
|
21
|
-
log('LangChain DocxLoader created');
|
|
22
|
-
const docs = await loader.load(); // Langchain DocxLoader typically loads the whole doc as one
|
|
23
|
-
log('DOCX document loaded, parts:', docs.length);
|
|
24
|
-
|
|
25
|
-
const pages: DocumentPage[] = docs.map((doc) => {
|
|
26
|
-
const pageContent = doc.pageContent || '';
|
|
27
|
-
const lines = pageContent.split('\n');
|
|
28
|
-
const lineCount = lines.length;
|
|
29
|
-
const charCount = pageContent.length;
|
|
17
|
+
// Read file as buffer
|
|
18
|
+
const buffer = await fs.readFile(filePath);
|
|
19
|
+
log('File buffer read, size:', buffer.length);
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pageNumber: 1,
|
|
36
|
-
};
|
|
21
|
+
// Extract text using mammoth
|
|
22
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
23
|
+
const pageContent = result.value;
|
|
24
|
+
log('Text extracted, length:', pageContent.length);
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
// Count lines and characters
|
|
27
|
+
const lines = pageContent.split('\n');
|
|
28
|
+
const lineCount = lines.length;
|
|
29
|
+
const charCount = pageContent.length;
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
log('DOCX document processed, lines:', lineCount, 'chars:', charCount);
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
// Create single page with extracted content
|
|
34
|
+
const page: DocumentPage = {
|
|
35
|
+
charCount,
|
|
36
|
+
lineCount,
|
|
37
|
+
metadata: {
|
|
38
|
+
pageNumber: 1,
|
|
39
|
+
},
|
|
40
|
+
pageContent,
|
|
41
|
+
};
|
|
50
42
|
|
|
51
|
-
//
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pageContent: '',
|
|
59
|
-
});
|
|
43
|
+
// Handle warnings if any
|
|
44
|
+
if (result.messages.length > 0) {
|
|
45
|
+
const warnings = result.messages.filter((msg) => msg.type === 'warning');
|
|
46
|
+
if (warnings.length > 0) {
|
|
47
|
+
log('Extraction warnings:', warnings.length);
|
|
48
|
+
warnings.forEach((warning) => log('Warning:', warning.message));
|
|
49
|
+
}
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
log('DOCX loading completed
|
|
63
|
-
return
|
|
52
|
+
log('DOCX loading completed');
|
|
53
|
+
return [page];
|
|
64
54
|
} catch (e) {
|
|
65
55
|
const error = e as Error;
|
|
66
56
|
log('Error encountered while loading DOCX file');
|
|
67
|
-
console.error(`Error loading DOCX file ${filePath}
|
|
57
|
+
console.error(`Error loading DOCX file ${filePath}: ${error.message}`);
|
|
58
|
+
|
|
68
59
|
const errorPage: DocumentPage = {
|
|
69
60
|
charCount: 0,
|
|
70
61
|
lineCount: 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FileLoaderInterface, SupportedFileType } from '../types';
|
|
2
|
+
import { DocLoader } from './doc';
|
|
2
3
|
import { DocxLoader } from './docx';
|
|
3
4
|
// import { EpubLoader } from './epub';
|
|
4
5
|
import { ExcelLoader } from './excel';
|
|
@@ -10,6 +11,7 @@ import { TextLoader } from './text';
|
|
|
10
11
|
// Key: file extension (lowercase, without leading dot) or specific type name
|
|
11
12
|
// Value: Loader Class implementing FileLoaderInterface
|
|
12
13
|
export const fileLoaders: Record<SupportedFileType, new () => FileLoaderInterface> = {
|
|
14
|
+
doc: DocLoader,
|
|
13
15
|
docx: DocxLoader,
|
|
14
16
|
// epub: EpubLoader,
|
|
15
17
|
excel: ExcelLoader,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Define supported file types - consider using an enum or const assertion
|
|
2
|
-
export type SupportedFileType = 'pdf' | 'docx' | 'txt' | 'excel' | 'pptx'; // | 'pptx' | 'latex' | 'epub' | 'code' | 'markdown';
|
|
2
|
+
export type SupportedFileType = 'pdf' | 'doc' | 'docx' | 'txt' | 'excel' | 'pptx'; // | 'pptx' | 'latex' | 'epub' | 'code' | 'markdown';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 代表一个完整的已加载文件,包含文件级信息和其所有页面/块。
|
|
@@ -649,7 +649,6 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
649
649
|
it('should return bizErrorType with the cause when OpenAI.APIError is thrown with cause', async () => {
|
|
650
650
|
// Arrange
|
|
651
651
|
const errorInfo = {
|
|
652
|
-
stack: 'abc',
|
|
653
652
|
cause: {
|
|
654
653
|
message: 'api is undefined',
|
|
655
654
|
},
|
|
@@ -670,7 +669,6 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
670
669
|
endpoint: defaultBaseURL,
|
|
671
670
|
error: {
|
|
672
671
|
cause: { message: 'api is undefined' },
|
|
673
|
-
stack: 'abc',
|
|
674
672
|
},
|
|
675
673
|
errorType: bizErrorType,
|
|
676
674
|
provider,
|
|
@@ -681,7 +679,6 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
681
679
|
it('should return bizErrorType with an cause response with desensitize Url', async () => {
|
|
682
680
|
// Arrange
|
|
683
681
|
const errorInfo = {
|
|
684
|
-
stack: 'abc',
|
|
685
682
|
cause: { message: 'api is undefined' },
|
|
686
683
|
};
|
|
687
684
|
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
|
@@ -706,7 +703,6 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
706
703
|
endpoint: 'https://api.***.com/v1',
|
|
707
704
|
error: {
|
|
708
705
|
cause: { message: 'api is undefined' },
|
|
709
|
-
stack: 'abc',
|
|
710
706
|
},
|
|
711
707
|
errorType: bizErrorType,
|
|
712
708
|
provider,
|
|
@@ -780,7 +776,6 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
780
776
|
name: genericError.name,
|
|
781
777
|
cause: genericError.cause,
|
|
782
778
|
message: genericError.message,
|
|
783
|
-
stack: genericError.stack,
|
|
784
779
|
},
|
|
785
780
|
});
|
|
786
781
|
}
|
|
@@ -1444,8 +1439,13 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1444
1439
|
const payload = {
|
|
1445
1440
|
messages: [{ content: 'Generate a person object', role: 'user' as const }],
|
|
1446
1441
|
schema: {
|
|
1447
|
-
|
|
1448
|
-
|
|
1442
|
+
name: 'person_extractor',
|
|
1443
|
+
description: 'Extract person information',
|
|
1444
|
+
schema: {
|
|
1445
|
+
type: 'object' as const,
|
|
1446
|
+
properties: { name: { type: 'string' }, age: { type: 'number' } },
|
|
1447
|
+
},
|
|
1448
|
+
strict: true,
|
|
1449
1449
|
},
|
|
1450
1450
|
model: 'gpt-4o',
|
|
1451
1451
|
responseApi: true,
|
|
@@ -1476,7 +1476,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1476
1476
|
|
|
1477
1477
|
const payload = {
|
|
1478
1478
|
messages: [{ content: 'Generate status', role: 'user' as const }],
|
|
1479
|
-
schema: {
|
|
1479
|
+
schema: {
|
|
1480
|
+
name: 'status_extractor',
|
|
1481
|
+
schema: { type: 'object' as const, properties: { status: { type: 'string' } } },
|
|
1482
|
+
},
|
|
1480
1483
|
model: 'gpt-4o',
|
|
1481
1484
|
responseApi: true,
|
|
1482
1485
|
};
|
|
@@ -1513,7 +1516,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1513
1516
|
|
|
1514
1517
|
const payload = {
|
|
1515
1518
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1516
|
-
schema: {
|
|
1519
|
+
schema: {
|
|
1520
|
+
name: 'test_tool',
|
|
1521
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1522
|
+
},
|
|
1517
1523
|
model: 'gpt-4o',
|
|
1518
1524
|
responseApi: true,
|
|
1519
1525
|
};
|
|
@@ -1536,7 +1542,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1536
1542
|
|
|
1537
1543
|
const payload = {
|
|
1538
1544
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1539
|
-
schema: {
|
|
1545
|
+
schema: {
|
|
1546
|
+
name: 'test_tool',
|
|
1547
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1548
|
+
},
|
|
1540
1549
|
model: 'gpt-4o',
|
|
1541
1550
|
responseApi: true,
|
|
1542
1551
|
};
|
|
@@ -1560,22 +1569,25 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1560
1569
|
const payload = {
|
|
1561
1570
|
messages: [{ content: 'Generate complex user data', role: 'user' as const }],
|
|
1562
1571
|
schema: {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
type: '
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1572
|
+
name: 'user_extractor',
|
|
1573
|
+
schema: {
|
|
1574
|
+
type: 'object' as const,
|
|
1575
|
+
properties: {
|
|
1576
|
+
user: {
|
|
1577
|
+
type: 'object',
|
|
1578
|
+
properties: {
|
|
1579
|
+
name: { type: 'string' },
|
|
1580
|
+
profile: {
|
|
1581
|
+
type: 'object',
|
|
1582
|
+
properties: {
|
|
1583
|
+
age: { type: 'number' },
|
|
1584
|
+
preferences: { type: 'array', items: { type: 'string' } },
|
|
1585
|
+
},
|
|
1574
1586
|
},
|
|
1575
1587
|
},
|
|
1576
1588
|
},
|
|
1589
|
+
metadata: { type: 'object' },
|
|
1577
1590
|
},
|
|
1578
|
-
metadata: { type: 'object' },
|
|
1579
1591
|
},
|
|
1580
1592
|
},
|
|
1581
1593
|
model: 'gpt-4o',
|
|
@@ -1605,7 +1617,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1605
1617
|
|
|
1606
1618
|
const payload = {
|
|
1607
1619
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1608
|
-
schema: {
|
|
1620
|
+
schema: {
|
|
1621
|
+
name: 'test_tool',
|
|
1622
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1623
|
+
},
|
|
1609
1624
|
model: 'gpt-4o',
|
|
1610
1625
|
responseApi: true,
|
|
1611
1626
|
};
|
|
@@ -1634,8 +1649,11 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1634
1649
|
const payload = {
|
|
1635
1650
|
messages: [{ content: 'Generate a person object', role: 'user' as const }],
|
|
1636
1651
|
schema: {
|
|
1637
|
-
|
|
1638
|
-
|
|
1652
|
+
name: 'person_extractor',
|
|
1653
|
+
schema: {
|
|
1654
|
+
type: 'object' as const,
|
|
1655
|
+
properties: { name: { type: 'string' }, age: { type: 'number' } },
|
|
1656
|
+
},
|
|
1639
1657
|
},
|
|
1640
1658
|
model: 'gpt-4o',
|
|
1641
1659
|
// responseApi: false or undefined - uses chat completions API
|
|
@@ -1673,7 +1691,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1673
1691
|
|
|
1674
1692
|
const payload = {
|
|
1675
1693
|
messages: [{ content: 'Generate status', role: 'user' as const }],
|
|
1676
|
-
schema: {
|
|
1694
|
+
schema: {
|
|
1695
|
+
name: 'status_extractor',
|
|
1696
|
+
schema: { type: 'object' as const, properties: { status: { type: 'string' } } },
|
|
1697
|
+
},
|
|
1677
1698
|
model: 'gpt-4o',
|
|
1678
1699
|
responseApi: false,
|
|
1679
1700
|
};
|
|
@@ -1717,7 +1738,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1717
1738
|
|
|
1718
1739
|
const payload = {
|
|
1719
1740
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1720
|
-
schema: {
|
|
1741
|
+
schema: {
|
|
1742
|
+
name: 'test_tool',
|
|
1743
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1744
|
+
},
|
|
1721
1745
|
model: 'gpt-4o',
|
|
1722
1746
|
responseApi: false,
|
|
1723
1747
|
};
|
|
@@ -1748,7 +1772,10 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1748
1772
|
|
|
1749
1773
|
const payload = {
|
|
1750
1774
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1751
|
-
schema: {
|
|
1775
|
+
schema: {
|
|
1776
|
+
name: 'test_tool',
|
|
1777
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1778
|
+
},
|
|
1752
1779
|
model: 'gpt-4o',
|
|
1753
1780
|
responseApi: false,
|
|
1754
1781
|
};
|
|
@@ -1780,19 +1807,22 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1780
1807
|
const payload = {
|
|
1781
1808
|
messages: [{ content: 'Generate items list', role: 'user' as const }],
|
|
1782
1809
|
schema: {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1810
|
+
name: 'abc',
|
|
1811
|
+
schema: {
|
|
1812
|
+
type: 'object' as const,
|
|
1813
|
+
properties: {
|
|
1787
1814
|
items: {
|
|
1788
|
-
type: '
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1815
|
+
type: 'array',
|
|
1816
|
+
items: {
|
|
1817
|
+
type: 'object',
|
|
1818
|
+
properties: {
|
|
1819
|
+
id: { type: 'number' },
|
|
1820
|
+
name: { type: 'string' },
|
|
1821
|
+
},
|
|
1792
1822
|
},
|
|
1793
1823
|
},
|
|
1824
|
+
total: { type: 'number' },
|
|
1794
1825
|
},
|
|
1795
|
-
total: { type: 'number' },
|
|
1796
1826
|
},
|
|
1797
1827
|
},
|
|
1798
1828
|
model: 'gpt-4o',
|
|
@@ -1816,7 +1846,7 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1816
1846
|
|
|
1817
1847
|
const payload = {
|
|
1818
1848
|
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1819
|
-
schema: { type: 'object' },
|
|
1849
|
+
schema: { name: 'abc', schema: { type: 'object' } as any },
|
|
1820
1850
|
model: 'gpt-4o',
|
|
1821
1851
|
responseApi: false,
|
|
1822
1852
|
};
|
|
@@ -1826,6 +1856,205 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
1826
1856
|
);
|
|
1827
1857
|
});
|
|
1828
1858
|
});
|
|
1859
|
+
|
|
1860
|
+
describe('tool calling fallback', () => {
|
|
1861
|
+
let instanceWithToolCalling: any;
|
|
1862
|
+
|
|
1863
|
+
beforeEach(() => {
|
|
1864
|
+
const RuntimeClass = createOpenAICompatibleRuntime({
|
|
1865
|
+
baseURL: 'https://api.test.com',
|
|
1866
|
+
generateObject: {
|
|
1867
|
+
useToolsCalling: true,
|
|
1868
|
+
},
|
|
1869
|
+
provider: 'test-provider',
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
instanceWithToolCalling = new RuntimeClass({ apiKey: 'test-key' });
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
it('should use tool calling when configured', async () => {
|
|
1876
|
+
const mockResponse = {
|
|
1877
|
+
choices: [
|
|
1878
|
+
{
|
|
1879
|
+
message: {
|
|
1880
|
+
tool_calls: [
|
|
1881
|
+
{
|
|
1882
|
+
type: 'function' as const,
|
|
1883
|
+
function: {
|
|
1884
|
+
name: 'person_extractor',
|
|
1885
|
+
arguments: '{"name":"Alice","age":28}',
|
|
1886
|
+
},
|
|
1887
|
+
},
|
|
1888
|
+
],
|
|
1889
|
+
},
|
|
1890
|
+
},
|
|
1891
|
+
],
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
|
|
1895
|
+
mockResponse as any,
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
const payload = {
|
|
1899
|
+
messages: [{ content: 'Extract person info', role: 'user' as const }],
|
|
1900
|
+
schema: {
|
|
1901
|
+
name: 'person_extractor',
|
|
1902
|
+
description: 'Extract person information',
|
|
1903
|
+
schema: {
|
|
1904
|
+
type: 'object' as const,
|
|
1905
|
+
properties: { name: { type: 'string' }, age: { type: 'number' } },
|
|
1906
|
+
},
|
|
1907
|
+
},
|
|
1908
|
+
model: 'test-model',
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const result = await instanceWithToolCalling.generateObject(payload);
|
|
1912
|
+
|
|
1913
|
+
expect(instanceWithToolCalling['client'].chat.completions.create).toHaveBeenCalledWith(
|
|
1914
|
+
{
|
|
1915
|
+
messages: payload.messages,
|
|
1916
|
+
model: payload.model,
|
|
1917
|
+
tools: [
|
|
1918
|
+
{
|
|
1919
|
+
type: 'function',
|
|
1920
|
+
function: {
|
|
1921
|
+
name: 'person_extractor',
|
|
1922
|
+
description: 'Extract person information',
|
|
1923
|
+
parameters: payload.schema.schema,
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1926
|
+
],
|
|
1927
|
+
tool_choice: { type: 'function', function: { name: 'person_extractor' } },
|
|
1928
|
+
user: undefined,
|
|
1929
|
+
},
|
|
1930
|
+
{ headers: undefined, signal: undefined },
|
|
1931
|
+
);
|
|
1932
|
+
|
|
1933
|
+
expect(result).toEqual({ name: 'Alice', age: 28 });
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it('should return undefined when no tool call found', async () => {
|
|
1937
|
+
const mockResponse = {
|
|
1938
|
+
choices: [
|
|
1939
|
+
{
|
|
1940
|
+
message: {
|
|
1941
|
+
content: 'Some text response',
|
|
1942
|
+
},
|
|
1943
|
+
},
|
|
1944
|
+
],
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
|
|
1948
|
+
mockResponse as any,
|
|
1949
|
+
);
|
|
1950
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1951
|
+
|
|
1952
|
+
const payload = {
|
|
1953
|
+
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1954
|
+
schema: {
|
|
1955
|
+
name: 'test_tool',
|
|
1956
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1957
|
+
},
|
|
1958
|
+
model: 'test-model',
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
const result = await instanceWithToolCalling.generateObject(payload);
|
|
1962
|
+
|
|
1963
|
+
expect(consoleSpy).toHaveBeenCalledWith('No tool call found in response');
|
|
1964
|
+
expect(result).toBeUndefined();
|
|
1965
|
+
|
|
1966
|
+
consoleSpy.mockRestore();
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
it('should return undefined when tool call arguments parsing fails', async () => {
|
|
1970
|
+
const mockResponse = {
|
|
1971
|
+
choices: [
|
|
1972
|
+
{
|
|
1973
|
+
message: {
|
|
1974
|
+
tool_calls: [
|
|
1975
|
+
{
|
|
1976
|
+
type: 'function' as const,
|
|
1977
|
+
function: {
|
|
1978
|
+
name: 'test_tool',
|
|
1979
|
+
arguments: 'invalid json',
|
|
1980
|
+
},
|
|
1981
|
+
},
|
|
1982
|
+
],
|
|
1983
|
+
},
|
|
1984
|
+
},
|
|
1985
|
+
],
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
|
|
1989
|
+
mockResponse as any,
|
|
1990
|
+
);
|
|
1991
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1992
|
+
|
|
1993
|
+
const payload = {
|
|
1994
|
+
messages: [{ content: 'Generate data', role: 'user' as const }],
|
|
1995
|
+
schema: {
|
|
1996
|
+
name: 'test_tool',
|
|
1997
|
+
schema: { type: 'object' as const, properties: {} },
|
|
1998
|
+
},
|
|
1999
|
+
model: 'test-model',
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
const result = await instanceWithToolCalling.generateObject(payload);
|
|
2003
|
+
|
|
2004
|
+
expect(consoleSpy).toHaveBeenCalledWith('parse tool call arguments error:', 'invalid json');
|
|
2005
|
+
expect(result).toBeUndefined();
|
|
2006
|
+
|
|
2007
|
+
consoleSpy.mockRestore();
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
it('should handle options correctly with tool calling', async () => {
|
|
2011
|
+
const mockResponse = {
|
|
2012
|
+
choices: [
|
|
2013
|
+
{
|
|
2014
|
+
message: {
|
|
2015
|
+
tool_calls: [
|
|
2016
|
+
{
|
|
2017
|
+
type: 'function' as const,
|
|
2018
|
+
function: {
|
|
2019
|
+
name: 'data_extractor',
|
|
2020
|
+
arguments: '{"data":"test"}',
|
|
2021
|
+
},
|
|
2022
|
+
},
|
|
2023
|
+
],
|
|
2024
|
+
},
|
|
2025
|
+
},
|
|
2026
|
+
],
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
vi.spyOn(instanceWithToolCalling['client'].chat.completions, 'create').mockResolvedValue(
|
|
2030
|
+
mockResponse as any,
|
|
2031
|
+
);
|
|
2032
|
+
|
|
2033
|
+
const payload = {
|
|
2034
|
+
messages: [{ content: 'Extract data', role: 'user' as const }],
|
|
2035
|
+
schema: {
|
|
2036
|
+
name: 'data_extractor',
|
|
2037
|
+
schema: { type: 'object' as const, properties: { data: { type: 'string' } } },
|
|
2038
|
+
},
|
|
2039
|
+
model: 'test-model',
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
const options = {
|
|
2043
|
+
headers: { 'X-Custom': 'header' },
|
|
2044
|
+
user: 'test-user',
|
|
2045
|
+
signal: new AbortController().signal,
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
const result = await instanceWithToolCalling.generateObject(payload, options);
|
|
2049
|
+
|
|
2050
|
+
expect(instanceWithToolCalling['client'].chat.completions.create).toHaveBeenCalledWith(
|
|
2051
|
+
expect.any(Object),
|
|
2052
|
+
{ headers: options.headers, signal: options.signal },
|
|
2053
|
+
);
|
|
2054
|
+
|
|
2055
|
+
expect(result).toEqual({ data: 'test' });
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
1829
2058
|
});
|
|
1830
2059
|
|
|
1831
2060
|
describe('models', () => {
|
|
@@ -116,6 +116,12 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
|
|
116
116
|
bizError: ILobeAgentRuntimeErrorType;
|
|
117
117
|
invalidAPIKey: ILobeAgentRuntimeErrorType;
|
|
118
118
|
};
|
|
119
|
+
generateObject?: {
|
|
120
|
+
/**
|
|
121
|
+
* Use tool calling to simulate structured output for providers that don't support native structured output
|
|
122
|
+
*/
|
|
123
|
+
useToolsCalling?: boolean;
|
|
124
|
+
};
|
|
119
125
|
models?:
|
|
120
126
|
| ((params: { client: OpenAI }) => Promise<ChatModelCard[]>)
|
|
121
127
|
| {
|
|
@@ -142,6 +148,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
142
148
|
customClient,
|
|
143
149
|
responses,
|
|
144
150
|
createImage: customCreateImage,
|
|
151
|
+
generateObject: generateObjectConfig,
|
|
145
152
|
}: OpenAICompatibleFactoryOptions<T>) => {
|
|
146
153
|
const ErrorType = {
|
|
147
154
|
bizError: errorType?.bizError || AgentRuntimeErrorType.ProviderBizError,
|
|
@@ -391,6 +398,44 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
391
398
|
async generateObject(payload: GenerateObjectPayload, options?: GenerateObjectOptions) {
|
|
392
399
|
const { messages, schema, model, responseApi } = payload;
|
|
393
400
|
|
|
401
|
+
// Use tool calling fallback if configured
|
|
402
|
+
if (generateObjectConfig?.useToolsCalling) {
|
|
403
|
+
const tool: ChatCompletionTool = {
|
|
404
|
+
function: {
|
|
405
|
+
description:
|
|
406
|
+
schema.description || 'Generate structured output according to the provided schema',
|
|
407
|
+
name: schema.name || 'structured_output',
|
|
408
|
+
parameters: schema.schema,
|
|
409
|
+
},
|
|
410
|
+
type: 'function',
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const res = await this.client.chat.completions.create(
|
|
414
|
+
{
|
|
415
|
+
messages,
|
|
416
|
+
model,
|
|
417
|
+
tool_choice: { function: { name: tool.function.name }, type: 'function' },
|
|
418
|
+
tools: [tool],
|
|
419
|
+
user: options?.user,
|
|
420
|
+
},
|
|
421
|
+
{ headers: options?.headers, signal: options?.signal },
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const toolCall = res.choices[0].message.tool_calls?.[0];
|
|
425
|
+
|
|
426
|
+
if (!toolCall || toolCall.type !== 'function') {
|
|
427
|
+
console.error('No tool call found in response');
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(toolCall.function.arguments);
|
|
433
|
+
} catch {
|
|
434
|
+
console.error('parse tool call arguments error:', toolCall.function.arguments);
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
394
439
|
if (responseApi) {
|
|
395
440
|
const res = await this.client!.responses.create(
|
|
396
441
|
{
|
|
@@ -160,7 +160,6 @@ export const testProvider = ({
|
|
|
160
160
|
cause: {
|
|
161
161
|
message: 'api is undefined',
|
|
162
162
|
},
|
|
163
|
-
stack: 'abc',
|
|
164
163
|
};
|
|
165
164
|
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
|
166
165
|
|
|
@@ -178,7 +177,6 @@ export const testProvider = ({
|
|
|
178
177
|
endpoint: defaultBaseURL,
|
|
179
178
|
error: {
|
|
180
179
|
cause: { message: 'api is undefined' },
|
|
181
|
-
stack: 'abc',
|
|
182
180
|
},
|
|
183
181
|
errorType: bizErrorType,
|
|
184
182
|
provider,
|
|
@@ -190,7 +188,6 @@ export const testProvider = ({
|
|
|
190
188
|
// Arrange
|
|
191
189
|
const errorInfo = {
|
|
192
190
|
cause: { message: 'api is undefined' },
|
|
193
|
-
stack: 'abc',
|
|
194
191
|
};
|
|
195
192
|
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
|
196
193
|
|
|
@@ -214,7 +211,6 @@ export const testProvider = ({
|
|
|
214
211
|
endpoint: 'https://api.***.com/v1',
|
|
215
212
|
error: {
|
|
216
213
|
cause: { message: 'api is undefined' },
|
|
217
|
-
stack: 'abc',
|
|
218
214
|
},
|
|
219
215
|
errorType: bizErrorType,
|
|
220
216
|
provider,
|
|
@@ -265,7 +261,6 @@ export const testProvider = ({
|
|
|
265
261
|
cause: genericError.cause,
|
|
266
262
|
message: genericError.message,
|
|
267
263
|
name: genericError.name,
|
|
268
|
-
stack: genericError.stack,
|
|
269
264
|
},
|
|
270
265
|
errorType: 'AgentRuntimeError',
|
|
271
266
|
provider,
|