@ai-sdk/langchain 2.0.213 → 2.0.215
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/CHANGELOG.md +13 -0
- package/dist/index.js +122 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +122 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +15 -0
- package/src/types.ts +14 -0
- package/src/utils.ts +191 -7
package/src/types.ts
CHANGED
|
@@ -28,6 +28,20 @@ export interface LangGraphEventState {
|
|
|
28
28
|
currentStep: number | null;
|
|
29
29
|
/** Maps tool call key (name:argsJson) to tool call ID for HITL interrupt handling */
|
|
30
30
|
emittedToolCallsByKey: Map<string, string>;
|
|
31
|
+
/** Tracks source IDs already emitted to avoid duplicates across messages/values events */
|
|
32
|
+
emittedSourceIds: Set<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A LangChain citation projected to the fields the AI SDK source parts can carry.
|
|
37
|
+
*/
|
|
38
|
+
export interface NormalizedCitation {
|
|
39
|
+
url?: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
source?: string;
|
|
42
|
+
citedText?: string;
|
|
43
|
+
startIndex?: number;
|
|
44
|
+
endIndex?: number;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -14,15 +14,18 @@ import {
|
|
|
14
14
|
type ToolResultPart,
|
|
15
15
|
type AssistantContent,
|
|
16
16
|
type UserContent,
|
|
17
|
+
type ProviderMetadata,
|
|
18
|
+
type JSONValue,
|
|
17
19
|
} from 'ai';
|
|
18
20
|
|
|
19
|
-
import
|
|
20
|
-
LangGraphEventState,
|
|
21
|
-
LangGraphMessageSeen,
|
|
22
|
-
ReasoningContentBlock,
|
|
23
|
-
ThinkingContentBlock,
|
|
24
|
-
GPT5ReasoningOutput,
|
|
25
|
-
ImageGenerationOutput,
|
|
21
|
+
import {
|
|
22
|
+
type LangGraphEventState,
|
|
23
|
+
type LangGraphMessageSeen,
|
|
24
|
+
type ReasoningContentBlock,
|
|
25
|
+
type ThinkingContentBlock,
|
|
26
|
+
type GPT5ReasoningOutput,
|
|
27
|
+
type ImageGenerationOutput,
|
|
28
|
+
type NormalizedCitation,
|
|
26
29
|
} from './types';
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -394,6 +397,7 @@ export function processModelChunk(
|
|
|
394
397
|
/** Track the ID used for text-start to ensure text-end uses the same ID */
|
|
395
398
|
textMessageId?: string | null;
|
|
396
399
|
emittedImages?: Set<string>;
|
|
400
|
+
emittedSourceIds?: Set<string>;
|
|
397
401
|
},
|
|
398
402
|
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
399
403
|
): void {
|
|
@@ -404,6 +408,10 @@ export function processModelChunk(
|
|
|
404
408
|
state.emittedImages = new Set<string>();
|
|
405
409
|
}
|
|
406
410
|
|
|
411
|
+
if (!state.emittedSourceIds) {
|
|
412
|
+
state.emittedSourceIds = new Set<string>();
|
|
413
|
+
}
|
|
414
|
+
|
|
407
415
|
/**
|
|
408
416
|
* Get the message ID from the chunk if available
|
|
409
417
|
*/
|
|
@@ -507,6 +515,16 @@ export function processModelChunk(
|
|
|
507
515
|
id: state.textMessageId ?? state.messageId,
|
|
508
516
|
});
|
|
509
517
|
}
|
|
518
|
+
|
|
519
|
+
const citations = extractCitationsFromContentBlocks(chunk);
|
|
520
|
+
if (citations.length > 0) {
|
|
521
|
+
emitSourceChunks(
|
|
522
|
+
citations,
|
|
523
|
+
state.messageId,
|
|
524
|
+
state.emittedSourceIds,
|
|
525
|
+
controller,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
510
528
|
}
|
|
511
529
|
|
|
512
530
|
/**
|
|
@@ -921,6 +939,152 @@ export function extractReasoningFromValuesMessage(
|
|
|
921
939
|
return undefined;
|
|
922
940
|
}
|
|
923
941
|
|
|
942
|
+
export function isCitationContentBlock(
|
|
943
|
+
obj: unknown,
|
|
944
|
+
): obj is ContentBlock.Citation {
|
|
945
|
+
return (
|
|
946
|
+
obj != null &&
|
|
947
|
+
typeof obj === 'object' &&
|
|
948
|
+
'type' in obj &&
|
|
949
|
+
(obj as { type: unknown }).type === 'citation'
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* LangChain Core standardizes citations as `{ type: 'citation', ... }` entries in
|
|
955
|
+
* the `annotations` array of a text content block. The text extractors elsewhere
|
|
956
|
+
* in this file only read the `text` field, so without this the citations would be
|
|
957
|
+
* dropped instead of surfaced as AI SDK source parts.
|
|
958
|
+
*
|
|
959
|
+
* Handles both class instances and serialized LangChain message objects.
|
|
960
|
+
*/
|
|
961
|
+
export function extractCitationsFromContentBlocks(
|
|
962
|
+
msg: unknown,
|
|
963
|
+
): NormalizedCitation[] {
|
|
964
|
+
if (msg == null || typeof msg !== 'object') return [];
|
|
965
|
+
|
|
966
|
+
const msgObj = msg as Record<string, unknown>;
|
|
967
|
+
const kwargs =
|
|
968
|
+
msgObj.kwargs && typeof msgObj.kwargs === 'object'
|
|
969
|
+
? (msgObj.kwargs as Record<string, unknown>)
|
|
970
|
+
: msgObj;
|
|
971
|
+
|
|
972
|
+
// `contentBlocks` is the normalized view derived from `content`; reading both
|
|
973
|
+
// would double-count annotations, so prefer it and only fall back to `content`.
|
|
974
|
+
const contentBlocks = (kwargs as { contentBlocks?: unknown }).contentBlocks;
|
|
975
|
+
const content = (kwargs as { content?: unknown }).content;
|
|
976
|
+
const blockSources: unknown[] = Array.isArray(contentBlocks)
|
|
977
|
+
? contentBlocks
|
|
978
|
+
: Array.isArray(content)
|
|
979
|
+
? content
|
|
980
|
+
: [];
|
|
981
|
+
|
|
982
|
+
const citations: NormalizedCitation[] = [];
|
|
983
|
+
|
|
984
|
+
for (const block of blockSources) {
|
|
985
|
+
if (block == null || typeof block !== 'object') continue;
|
|
986
|
+
|
|
987
|
+
const annotations = (block as { annotations?: unknown }).annotations;
|
|
988
|
+
if (!Array.isArray(annotations)) continue;
|
|
989
|
+
|
|
990
|
+
for (const annotation of annotations) {
|
|
991
|
+
if (!isCitationContentBlock(annotation)) continue;
|
|
992
|
+
|
|
993
|
+
citations.push({
|
|
994
|
+
url: annotation.url,
|
|
995
|
+
title: annotation.title,
|
|
996
|
+
source: annotation.source,
|
|
997
|
+
citedText: annotation.citedText,
|
|
998
|
+
startIndex: annotation.startIndex,
|
|
999
|
+
endIndex: annotation.endIndex,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return citations;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function buildSourceProviderMetadata(
|
|
1008
|
+
citation: NormalizedCitation,
|
|
1009
|
+
): ProviderMetadata | undefined {
|
|
1010
|
+
const langchain: Record<string, JSONValue> = {};
|
|
1011
|
+
|
|
1012
|
+
if (typeof citation.citedText === 'string') {
|
|
1013
|
+
langchain.citedText = citation.citedText;
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof citation.startIndex === 'number') {
|
|
1016
|
+
langchain.startIndex = citation.startIndex;
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof citation.endIndex === 'number') {
|
|
1019
|
+
langchain.endIndex = citation.endIndex;
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof citation.source === 'string') {
|
|
1022
|
+
langchain.source = citation.source;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (Object.keys(langchain).length === 0) {
|
|
1026
|
+
return undefined;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return { langchain };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Emits AI SDK source chunks (`source-url` / `source-document`) for the given
|
|
1034
|
+
* citations.
|
|
1035
|
+
*
|
|
1036
|
+
* Citations with a `url` become `source-url` parts keyed by that url; the rest
|
|
1037
|
+
* become `source-document` parts. Citation metadata that the source parts cannot
|
|
1038
|
+
* represent natively (`citedText`, `startIndex`, `endIndex`, `source`) is preserved
|
|
1039
|
+
* under `providerMetadata.langchain`.
|
|
1040
|
+
*
|
|
1041
|
+
* `emittedSourceIds` dedupes across the LangGraph messages and values events, which
|
|
1042
|
+
* can each surface the same citation. URL-less citations are keyed by their content
|
|
1043
|
+
* rather than position, so a differing citation subset/order between those two events
|
|
1044
|
+
* does not cause an id collision.
|
|
1045
|
+
*/
|
|
1046
|
+
export function emitSourceChunks(
|
|
1047
|
+
citations: NormalizedCitation[],
|
|
1048
|
+
messageId: string,
|
|
1049
|
+
emittedSourceIds: Set<string>,
|
|
1050
|
+
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
1051
|
+
): void {
|
|
1052
|
+
for (const citation of citations) {
|
|
1053
|
+
if (citation.url) {
|
|
1054
|
+
if (emittedSourceIds.has(citation.url)) continue;
|
|
1055
|
+
emittedSourceIds.add(citation.url);
|
|
1056
|
+
|
|
1057
|
+
const providerMetadata = buildSourceProviderMetadata(citation);
|
|
1058
|
+
controller.enqueue({
|
|
1059
|
+
type: 'source-url',
|
|
1060
|
+
sourceId: citation.url,
|
|
1061
|
+
url: citation.url,
|
|
1062
|
+
...(citation.title ? { title: citation.title } : {}),
|
|
1063
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
1064
|
+
});
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// A citation with no url and no human-readable label carries no information a
|
|
1069
|
+
// UI could render, so skip it rather than emit a placeholder source.
|
|
1070
|
+
const title = citation.title ?? citation.source;
|
|
1071
|
+
if (!title) continue;
|
|
1072
|
+
|
|
1073
|
+
const sourceId = `${messageId}:${title}:${citation.citedText ?? ''}:${citation.startIndex ?? ''}:${citation.endIndex ?? ''}`;
|
|
1074
|
+
if (emittedSourceIds.has(sourceId)) continue;
|
|
1075
|
+
emittedSourceIds.add(sourceId);
|
|
1076
|
+
|
|
1077
|
+
const providerMetadata = buildSourceProviderMetadata(citation);
|
|
1078
|
+
controller.enqueue({
|
|
1079
|
+
type: 'source-document',
|
|
1080
|
+
sourceId,
|
|
1081
|
+
mediaType: 'text/plain',
|
|
1082
|
+
title,
|
|
1083
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
924
1088
|
/**
|
|
925
1089
|
* Checks if an object is an image generation output
|
|
926
1090
|
*
|
|
@@ -1293,6 +1457,16 @@ export function processLangGraphEvent(
|
|
|
1293
1457
|
id: msgId,
|
|
1294
1458
|
});
|
|
1295
1459
|
}
|
|
1460
|
+
|
|
1461
|
+
const citations = extractCitationsFromContentBlocks(msg);
|
|
1462
|
+
if (citations.length > 0) {
|
|
1463
|
+
emitSourceChunks(
|
|
1464
|
+
citations,
|
|
1465
|
+
msgId,
|
|
1466
|
+
state.emittedSourceIds,
|
|
1467
|
+
controller,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1296
1470
|
} else if (isToolMessageType(msg)) {
|
|
1297
1471
|
// Handle both direct properties and serialized messages (kwargs)
|
|
1298
1472
|
const msgObj = msg as unknown as Record<string, unknown>;
|
|
@@ -1572,6 +1746,16 @@ export function processLangGraphEvent(
|
|
|
1572
1746
|
emittedReasoningIds.add(reasoningId);
|
|
1573
1747
|
}
|
|
1574
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
const valuesCitations = extractCitationsFromContentBlocks(msg);
|
|
1751
|
+
if (valuesCitations.length > 0) {
|
|
1752
|
+
emitSourceChunks(
|
|
1753
|
+
valuesCitations,
|
|
1754
|
+
msgId,
|
|
1755
|
+
state.emittedSourceIds,
|
|
1756
|
+
controller,
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1575
1759
|
}
|
|
1576
1760
|
}
|
|
1577
1761
|
}
|