@datacules/agent-identity-langchain 0.2.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.
- package/package.json +27 -0
- package/src/index.ts +175 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@datacules/agent-identity-langchain",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "LangChain and LangGraph integration for @datacules/agent-identity",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.build.json",
|
|
11
|
+
"type-check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@datacules/agent-identity": "^0.1.0",
|
|
15
|
+
"@langchain/core": ">=0.2.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependenciesMeta": {
|
|
18
|
+
"@langchain/core": {
|
|
19
|
+
"optional": false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@datacules/agent-identity": "*",
|
|
24
|
+
"@langchain/core": "^0.2.0",
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain integration for @datacules/agent-identity.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* createAgentIdentityModel() — wraps ChatOpenAI/ChatAnthropic with credential resolution
|
|
6
|
+
* AgentIdentityCallbackHandler — LangChain callback handler for audit logging
|
|
7
|
+
* createAgentIdentityNode() — LangGraph StateGraph node for credential resolution
|
|
8
|
+
*
|
|
9
|
+
* Usage (LangChain):
|
|
10
|
+
* const { getModel } = createAgentIdentityModel(ctx, credentials, rules, fetchSecret);
|
|
11
|
+
* const model = await getModel();
|
|
12
|
+
* const result = await model.invoke([{ role: 'user', content: 'Hello' }]);
|
|
13
|
+
*
|
|
14
|
+
* Usage (LangGraph):
|
|
15
|
+
* const graph = new StateGraph(...);
|
|
16
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules, logger));
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createRouter } from '@datacules/agent-identity';
|
|
20
|
+
import type {
|
|
21
|
+
AgentRequestContext,
|
|
22
|
+
AuditLogger,
|
|
23
|
+
Credential,
|
|
24
|
+
ResolvedCredential,
|
|
25
|
+
RoutingRule,
|
|
26
|
+
} from '@datacules/agent-identity';
|
|
27
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
28
|
+
import type { Serialized } from '@langchain/core/load/serializable';
|
|
29
|
+
import type { LLMResult } from '@langchain/core/outputs';
|
|
30
|
+
|
|
31
|
+
// ─── createAgentIdentityModel ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface AgentIdentityModelOptions {
|
|
34
|
+
credentials: Credential[];
|
|
35
|
+
rules: RoutingRule[];
|
|
36
|
+
/**
|
|
37
|
+
* Server-side secret fetcher. Receives the credential ref and returns
|
|
38
|
+
* the raw API key. NEVER call this client-side — use in a server route only.
|
|
39
|
+
*/
|
|
40
|
+
fetchSecret: (ref: string) => Promise<string>;
|
|
41
|
+
logger?: AuditLogger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AgentIdentityModelResult {
|
|
45
|
+
/** Resolved credential metadata (safe to log, never contains raw secret) */
|
|
46
|
+
resolved: ResolvedCredential;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the LangChain model instance with the API key injected.
|
|
49
|
+
* Dynamic import keeps @langchain/openai and @langchain/anthropic as
|
|
50
|
+
* optional peer deps — only the provider actually used is imported.
|
|
51
|
+
*/
|
|
52
|
+
getModel: () => Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a credential and return a factory that builds the correct
|
|
57
|
+
* LangChain chat model with the API key injected server-side.
|
|
58
|
+
*
|
|
59
|
+
* The raw secret is fetched via fetchSecret(ref) and passed directly to the
|
|
60
|
+
* model constructor. It never appears in a log, a response body, or the
|
|
61
|
+
* browser — the model is built on the server and used there.
|
|
62
|
+
*/
|
|
63
|
+
export function createAgentIdentityModel(
|
|
64
|
+
ctx: AgentRequestContext,
|
|
65
|
+
options: AgentIdentityModelOptions
|
|
66
|
+
): AgentIdentityModelResult {
|
|
67
|
+
const router = createRouter(options.credentials, options.rules, options.logger);
|
|
68
|
+
const resolved = router.resolve(ctx);
|
|
69
|
+
if (!resolved) throw new Error(`[agent-identity] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
70
|
+
|
|
71
|
+
const getModel = async (): Promise<unknown> => {
|
|
72
|
+
const apiKey = await options.fetchSecret(resolved.ref);
|
|
73
|
+
|
|
74
|
+
const meta = {
|
|
75
|
+
agentIdentityCredentialId: resolved.credentialId,
|
|
76
|
+
agentIdentityResolvedFor: resolved.resolvedFor,
|
|
77
|
+
traceId: ctx.traceId,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (ctx.provider === 'openai') {
|
|
81
|
+
const { ChatOpenAI } = await import('@langchain/openai' as string);
|
|
82
|
+
return new (ChatOpenAI as any)({ modelName: ctx.model, openAIApiKey: apiKey, metadata: meta });
|
|
83
|
+
}
|
|
84
|
+
if (ctx.provider === 'anthropic') {
|
|
85
|
+
const { ChatAnthropic } = await import('@langchain/anthropic' as string);
|
|
86
|
+
return new (ChatAnthropic as any)({ modelName: ctx.model, anthropicApiKey: apiKey, metadata: meta });
|
|
87
|
+
}
|
|
88
|
+
if (ctx.provider === 'gemini') {
|
|
89
|
+
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai' as string);
|
|
90
|
+
return new (ChatGoogleGenerativeAI as any)({ modelName: ctx.model, apiKey, metadata: meta });
|
|
91
|
+
}
|
|
92
|
+
if (ctx.provider === 'mistral') {
|
|
93
|
+
const { ChatMistralAI } = await import('@langchain/mistralai' as string);
|
|
94
|
+
return new (ChatMistralAI as any)({ modelName: ctx.model, apiKey, metadata: meta });
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`[agent-identity/langchain] Provider "${ctx.provider}" not yet supported in LangChain adapter.`);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { resolved, getModel };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── AgentIdentityCallbackHandler ────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* LangChain callback handler that records agent-identity resolution metadata
|
|
106
|
+
* into each LLM run's metadata. Drop this into any LangChain chain or agent
|
|
107
|
+
* to get automatic credential tracing without modifying the chain itself.
|
|
108
|
+
*
|
|
109
|
+
* Example:
|
|
110
|
+
* const handler = new AgentIdentityCallbackHandler(resolved, logger);
|
|
111
|
+
* await model.invoke(messages, { callbacks: [handler] });
|
|
112
|
+
*/
|
|
113
|
+
export class AgentIdentityCallbackHandler extends BaseCallbackHandler {
|
|
114
|
+
readonly name = 'AgentIdentityCallbackHandler';
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly resolved: ResolvedCredential,
|
|
118
|
+
private readonly logger?: AuditLogger
|
|
119
|
+
) {
|
|
120
|
+
super();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async handleLLMStart(
|
|
124
|
+
_llm: Serialized,
|
|
125
|
+
_prompts: string[],
|
|
126
|
+
runId: string,
|
|
127
|
+
_parentRunId?: string,
|
|
128
|
+
extraParams?: Record<string, unknown>
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if (extraParams) {
|
|
131
|
+
extraParams['agentIdentityCredentialId'] = this.resolved.credentialId;
|
|
132
|
+
extraParams['agentIdentityResolvedFor'] = this.resolved.resolvedFor;
|
|
133
|
+
}
|
|
134
|
+
// logger.log() is called by the router at resolve time; no duplicate log here
|
|
135
|
+
void runId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async handleLLMEnd(_output: LLMResult, _runId: string): Promise<void> {
|
|
139
|
+
// Extension point: log token usage, latency, etc.
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async handleLLMError(error: Error, _runId: string): Promise<void> {
|
|
143
|
+
console.warn('[agent-identity] LLM error with credential', this.resolved.ref, error.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── createAgentIdentityNode (LangGraph) ────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Drop-in LangGraph StateGraph node that resolves credentials before any
|
|
151
|
+
* LLM call in the graph. Attaches resolvedCredential to the state so
|
|
152
|
+
* downstream nodes can use it without repeating resolution.
|
|
153
|
+
*
|
|
154
|
+
* Example:
|
|
155
|
+
* const graph = new StateGraph(...);
|
|
156
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules));
|
|
157
|
+
* graph.addEdge('resolve-creds', 'llm-call');
|
|
158
|
+
*/
|
|
159
|
+
export function createAgentIdentityNode(
|
|
160
|
+
credentials: Credential[],
|
|
161
|
+
rules: RoutingRule[],
|
|
162
|
+
logger?: AuditLogger
|
|
163
|
+
) {
|
|
164
|
+
const router = createRouter(credentials, rules, logger);
|
|
165
|
+
|
|
166
|
+
return async (state: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
167
|
+
const ctx = state['agentContext'] as AgentRequestContext | undefined;
|
|
168
|
+
if (!ctx) throw new Error('[agent-identity/langchain] state.agentContext is required');
|
|
169
|
+
|
|
170
|
+
const resolved = router.resolve(ctx);
|
|
171
|
+
if (!resolved) throw new Error(`[agent-identity/langchain] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
172
|
+
|
|
173
|
+
return { ...state, resolvedCredential: resolved };
|
|
174
|
+
};
|
|
175
|
+
}
|