@arcproof/sdk-langchain 0.1.0
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/README.md +88 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator.d.ts +29 -0
- package/dist/orchestrator.js +72 -0
- package/dist/orchestrator.js.map +1 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @arcproof/sdk-langchain
|
|
2
|
+
|
|
3
|
+
LangChain.js adapter for [`@arcproof/sdk`](../sdk): turn any LangChain.js tool-calling agent into a `gatherClaims()` function usable by `runTrustedJob()`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @arcproof/sdk @arcproof/sdk-langchain @langchain/core @langchain/langgraph
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createLangChainClaimGatherer, combineClaimGatherers } from "@arcproof/sdk-langchain";
|
|
15
|
+
import { runTrustedJob, VerifierRegistry, ARC_TESTNET } from "@arcproof/sdk";
|
|
16
|
+
import { ChatGroq } from "@langchain/groq";
|
|
17
|
+
import { tool } from "@langchain/core/tools";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
const lookupTool = tool(
|
|
21
|
+
async ({ id }: { id: string }) => JSON.stringify(await lookUpSomething(id)),
|
|
22
|
+
{ name: "lookup", description: "...", schema: z.object({ id: z.string() }) }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const gatherClaims = createLangChainClaimGatherer({
|
|
26
|
+
agentId: "my-agent-v1",
|
|
27
|
+
model: new ChatGroq({ model: "llama-3.3-70b-versatile", apiKey: process.env.GROQ_API_KEY }),
|
|
28
|
+
tools: [lookupTool],
|
|
29
|
+
claimTypes: ["some_claim_type"], // optional -- omit for an open string
|
|
30
|
+
systemPrompt: "You are a ... specialist. Use your tools to gather claims, copy values verbatim.",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await runTrustedJob(
|
|
34
|
+
{ network: ARC_TESTNET, contractAddress, verifiers: new VerifierRegistry() /* .register(...) your rules */ },
|
|
35
|
+
{
|
|
36
|
+
jobId: "job-1",
|
|
37
|
+
budgetAmount: 0.05,
|
|
38
|
+
requester, settler, // WalletCredential
|
|
39
|
+
providerAddresses: { "my-agent-v1": "0x..." },
|
|
40
|
+
gatherClaims,
|
|
41
|
+
context: { id: "loan-001" },
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Multiple specialists, always engaged: `combineClaimGatherers([aprAgent, feeAgent, complianceAgent])`.
|
|
47
|
+
|
|
48
|
+
Multiple specialists, an LLM decides which ones this specific request needs (the orchestrator layer -- matches the reference apps' `orchestrator.ts`/`langchainPlanner.ts` pattern, generalized):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createLangChainOrchestrator, type SpecialistDescriptor } from "@arcproof/sdk-langchain";
|
|
52
|
+
|
|
53
|
+
const specialists: SpecialistDescriptor[] = [
|
|
54
|
+
{ id: "apr-agent-v1", description: "Checks true APR and fees -- engage for any cost question.", gatherClaims: aprGatherer },
|
|
55
|
+
{ id: "eligibility-agent-v1", description: "Checks borrower region eligibility -- engage for any eligibility question.", gatherClaims: eligibilityGatherer },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const gatherClaims = createLangChainOrchestrator({ model, specialists });
|
|
59
|
+
// A pure "what's the APR" request engages only apr-agent-v1.
|
|
60
|
+
// A request that also asks about eligibility engages both.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This is the full three-layer pattern, each with a clean separation of concerns:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
orchestrator (createLangChainOrchestrator) -- decides which specialists this request needs
|
|
67
|
+
specialist (createLangChainClaimGatherer) -- checks and drafts claims
|
|
68
|
+
evaluator (@arcproof/sdk's VerifierRegistry) -- independently verifies, zero LLM calls
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the planning call itself fails outright (not "returned zero valid ids," which defaults to engaging every specialist -- an actual throw), it propagates up to `runTrustedJob`'s existing refund path rather than silently falling back -- same "agent or loud failure" rule as everywhere else in this SDK.
|
|
72
|
+
|
|
73
|
+
## Why `claim_value`/`simulated` are plain strings in the structured-output schema
|
|
74
|
+
|
|
75
|
+
Two real, independently-observed provider incompatibilities, not a stylistic choice:
|
|
76
|
+
|
|
77
|
+
- Gemini's function-calling schema translator rejects JSON Schema `anyOf` unions nested inside array-of-object properties, at any branch count -- including the 2-branch `[string, null]` shape `.nullable()` produces.
|
|
78
|
+
- Groq has been observed returning the JSON string `"false"` for a `z.boolean()` field (`"expected boolean, but got string"`).
|
|
79
|
+
|
|
80
|
+
A plain string field with an explicit `"true"`/`"false"` convention, coerced back to a real boolean in JS, sidesteps both without depending on either provider fixing their schema translation. If you add new structured-output fields to a similar schema, keep this in mind.
|
|
81
|
+
|
|
82
|
+
## Resilience
|
|
83
|
+
|
|
84
|
+
If the underlying LLM call fails (quota, outage, malformed generation), `createLangChainClaimGatherer` logs and returns zero claims rather than throwing. `runTrustedJob`'s built-in `hasCheckableClaims` guard already handles "this provider contributed nothing" correctly (refund, not a false accept) -- a single flaky provider never has to crash the whole job.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
2
|
+
import type { StructuredToolInterface } from "@langchain/core/tools";
|
|
3
|
+
import type { Claim } from "@arcproof/sdk";
|
|
4
|
+
export interface LangChainClaimGathererOptions {
|
|
5
|
+
/** Becomes claim.provider_agent_id on every claim this agent drafts. */
|
|
6
|
+
agentId: string;
|
|
7
|
+
model: BaseChatModel;
|
|
8
|
+
tools: StructuredToolInterface[];
|
|
9
|
+
systemPrompt: string;
|
|
10
|
+
/** Restrict claim_type to this fixed set (z.enum) instead of an open string -- optional. */
|
|
11
|
+
claimTypes?: string[];
|
|
12
|
+
/** Builds the user message from the job context passed into gatherClaims(). Defaults to JSON.stringify(context). */
|
|
13
|
+
buildUserMessage?: (context: Record<string, unknown>) => string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a LangChain.js tool-calling agent as a `gatherClaims()` function.
|
|
17
|
+
* Resilient by design: an LLM call failure (quota, provider outage,
|
|
18
|
+
* malformed generation) logs and produces zero claims rather than
|
|
19
|
+
* throwing -- runTrustedJob()'s hasCheckableClaims() guard already
|
|
20
|
+
* handles "this provider contributed nothing" correctly (refund, not a
|
|
21
|
+
* false accept), so a single flaky provider never has to crash the job.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLangChainClaimGatherer(options: LangChainClaimGathererOptions): (context: Record<string, unknown>) => Promise<Claim[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Combines several claim gatherers (e.g. one LangChain agent per
|
|
26
|
+
* specialty) into the single gatherClaims() function runTrustedJob()
|
|
27
|
+
* expects -- runs them concurrently, and one gatherer throwing doesn't
|
|
28
|
+
* lose the others' claims.
|
|
29
|
+
*/
|
|
30
|
+
export declare function combineClaimGatherers(gatherers: Array<(context: Record<string, unknown>) => Promise<Claim[]>>): (context: Record<string, unknown>) => Promise<Claim[]>;
|
|
31
|
+
export * from "./orchestrator.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain.js adapter for @arcproof/sdk: turns a LangChain.js
|
|
3
|
+
* tool-calling agent into a `gatherClaims()` function runTrustedJob() can
|
|
4
|
+
* call directly. The agent decides which tools to call and drafts
|
|
5
|
+
* claims; this module only handles the LangChain-specific plumbing
|
|
6
|
+
* (structured output schema, resilience, coercion into real Claim
|
|
7
|
+
* objects) -- verification and payment stay entirely in @arcproof/sdk.
|
|
8
|
+
*
|
|
9
|
+
* The claim_value/simulated fields are deliberately typed as plain
|
|
10
|
+
* strings with an explicit convention (not z.boolean()/z.union([...]))
|
|
11
|
+
* in the structured-output schema below -- this sidesteps two real,
|
|
12
|
+
* independently-observed provider incompatibilities: Gemini's function-
|
|
13
|
+
* calling schema translator rejects `anyOf` unions nested inside
|
|
14
|
+
* array-of-object properties (any branch count, including 2-branch
|
|
15
|
+
* .nullable()), and Groq has been observed returning the JSON string
|
|
16
|
+
* "false" for a z.boolean() field ("expected boolean, but got string").
|
|
17
|
+
* A plain string field with a documented convention, coerced back to the
|
|
18
|
+
* real type in JS, avoids both without depending on either provider
|
|
19
|
+
* fixing their schema translation.
|
|
20
|
+
*/
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
|
24
|
+
function claimDraftSchema(claimTypes) {
|
|
25
|
+
return z.object({
|
|
26
|
+
claims: z.array(z.object({
|
|
27
|
+
claim_type: claimTypes && claimTypes.length > 0 ? z.enum(claimTypes) : z.string(),
|
|
28
|
+
claim_text: z.string().describe("Human-readable statement of the claim, citing the number/fact"),
|
|
29
|
+
claim_value: z
|
|
30
|
+
.string()
|
|
31
|
+
.describe('The exact value returned by a tool, copied verbatim as a string -- e.g. "182000000", "-3.4", "true". ' +
|
|
32
|
+
"Never estimated, never paraphrased."),
|
|
33
|
+
provider_source: z.string().describe("The exact source string/URL a tool returned"),
|
|
34
|
+
simulated: z
|
|
35
|
+
.string()
|
|
36
|
+
.default("false")
|
|
37
|
+
.describe('Literal lowercase "true" or "false" -- whether the underlying data was simulated/estimated rather than live.'),
|
|
38
|
+
})).describe("One entry per data point actually gathered -- omit any you couldn't produce, never invent one"),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Wraps a LangChain.js tool-calling agent as a `gatherClaims()` function.
|
|
43
|
+
* Resilient by design: an LLM call failure (quota, provider outage,
|
|
44
|
+
* malformed generation) logs and produces zero claims rather than
|
|
45
|
+
* throwing -- runTrustedJob()'s hasCheckableClaims() guard already
|
|
46
|
+
* handles "this provider contributed nothing" correctly (refund, not a
|
|
47
|
+
* false accept), so a single flaky provider never has to crash the job.
|
|
48
|
+
*/
|
|
49
|
+
export function createLangChainClaimGatherer(options) {
|
|
50
|
+
const schema = claimDraftSchema(options.claimTypes);
|
|
51
|
+
return async function gatherClaims(context) {
|
|
52
|
+
const jobId = context.jobId ?? randomUUID();
|
|
53
|
+
let drafts = [];
|
|
54
|
+
try {
|
|
55
|
+
const agent = createReactAgent({
|
|
56
|
+
llm: options.model,
|
|
57
|
+
tools: options.tools,
|
|
58
|
+
responseFormat: schema,
|
|
59
|
+
prompt: options.systemPrompt,
|
|
60
|
+
});
|
|
61
|
+
const userMsg = options.buildUserMessage ? options.buildUserMessage(context) : JSON.stringify(context);
|
|
62
|
+
const result = await agent.invoke({ messages: [{ role: "user", content: userMsg }] });
|
|
63
|
+
drafts = result.structuredResponse.claims;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.log(`[${options.agentId}] ! LLM agent unavailable (${e}) -- no claims produced this call`);
|
|
67
|
+
drafts = [];
|
|
68
|
+
}
|
|
69
|
+
return drafts.map((d) => {
|
|
70
|
+
const simulated = d.simulated?.toLowerCase() === "true";
|
|
71
|
+
console.log(`[${options.agentId}] ${d.claim_type} claim: ${d.claim_text} (simulated=${simulated})`);
|
|
72
|
+
return {
|
|
73
|
+
claim_id: randomUUID(),
|
|
74
|
+
job_id: jobId,
|
|
75
|
+
provider_agent_id: options.agentId,
|
|
76
|
+
claim_type: d.claim_type,
|
|
77
|
+
claim_text: d.claim_text,
|
|
78
|
+
claim_value: d.claim_value,
|
|
79
|
+
provider_source: d.provider_source,
|
|
80
|
+
simulated,
|
|
81
|
+
verification_status: "pending",
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Combines several claim gatherers (e.g. one LangChain agent per
|
|
88
|
+
* specialty) into the single gatherClaims() function runTrustedJob()
|
|
89
|
+
* expects -- runs them concurrently, and one gatherer throwing doesn't
|
|
90
|
+
* lose the others' claims.
|
|
91
|
+
*/
|
|
92
|
+
export function combineClaimGatherers(gatherers) {
|
|
93
|
+
return async function gatherClaims(context) {
|
|
94
|
+
const results = await Promise.all(gatherers.map((g) => g(context).catch((e) => {
|
|
95
|
+
console.log(`[combine-gatherers] ! a gatherer threw (${e}) -- treating as zero claims from it`);
|
|
96
|
+
return [];
|
|
97
|
+
})));
|
|
98
|
+
return results.flat();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export * from "./orchestrator.js";
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,SAAS,gBAAgB,CAAC,UAAqB;IAC7C,OAAO,CAAC,CAAC,MAAM,CAAC;QACd,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,MAAM,CAAC;YACP,UAAU,EAAE,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAmC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;YAC1G,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+DAA+D,CAAC;YAChG,WAAW,EAAE,CAAC;iBACX,MAAM,EAAE;iBACR,QAAQ,CACP,uGAAuG;gBACrG,qCAAqC,CACxC;YACH,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,6CAA6C,CAAC;YACnF,SAAS,EAAE,CAAC;iBACT,MAAM,EAAE;iBACR,OAAO,CAAC,OAAO,CAAC;iBAChB,QAAQ,CAAC,8GAA8G,CAAC;SAC5H,CAAC,CACH,CAAC,QAAQ,CAAC,+FAA+F,CAAC;KAC5G,CAAC,CAAC;AACL,CAAC;AAcD;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAAC,OAAsC;IACjF,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAEpD,OAAO,KAAK,UAAU,YAAY,CAAC,OAAgC;QACjE,MAAM,KAAK,GAAI,OAAO,CAAC,KAA4B,IAAI,UAAU,EAAE,CAAC;QACpE,IAAI,MAAM,GAAuH,EAAE,CAAC;QAEpI,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,gBAAgB,CAAC;gBAC7B,GAAG,EAAE,OAAO,CAAC,KAAK;gBAClB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,cAAc,EAAE,MAAM;gBACtB,MAAM,EAAE,OAAO,CAAC,YAAY;aAC7B,CAAC,CAAC;YACH,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YACvG,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YACtF,MAAM,GAAI,MAAM,CAAC,kBAAgD,CAAC,MAAM,CAAC;QAC3E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,gCAAgC,CAAC,mCAAmC,CAAC,CAAC;YACrG,MAAM,GAAG,EAAE,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACtB,MAAM,SAAS,GAAG,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,OAAO,CAAC,CAAC,UAAU,WAAW,CAAC,CAAC,UAAU,eAAe,SAAS,GAAG,CAAC,CAAC;YACtG,OAAO;gBACL,QAAQ,EAAE,UAAU,EAAE;gBACtB,MAAM,EAAE,KAAK;gBACb,iBAAiB,EAAE,OAAO,CAAC,OAAO;gBAClC,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,eAAe,EAAE,CAAC,CAAC,eAAe;gBAClC,SAAS;gBACT,mBAAmB,EAAE,SAAkB;aACxC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,SAAwE;IAExE,OAAO,KAAK,UAAU,YAAY,CAAC,OAAgC;QACjE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAClB,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;YACrB,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,sCAAsC,CAAC,CAAC;YAClG,OAAO,EAAa,CAAC;QACvB,CAAC,CAAC,CACH,CACF,CAAC;QACF,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC;AACJ,CAAC;AAED,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
2
|
+
import type { Claim } from "@arcproof/sdk";
|
|
3
|
+
export interface SpecialistDescriptor {
|
|
4
|
+
/** Becomes claim.provider_agent_id on every claim this specialist drafts (via its own gatherClaims). */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Shown to the planning LLM -- what this specialist checks, so it can decide when to engage it. */
|
|
7
|
+
description: string;
|
|
8
|
+
gatherClaims: (context: Record<string, unknown>) => Promise<Claim[]>;
|
|
9
|
+
}
|
|
10
|
+
export interface OrchestratorPlan {
|
|
11
|
+
specialist_ids: string[];
|
|
12
|
+
reasoning: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LangChainOrchestratorOptions {
|
|
15
|
+
model: BaseChatModel;
|
|
16
|
+
specialists: SpecialistDescriptor[];
|
|
17
|
+
/** Builds the planning call's user message from job context. Defaults to context.requestText, else JSON.stringify(context). */
|
|
18
|
+
buildPlanningMessage?: (context: Record<string, unknown>) => string;
|
|
19
|
+
/** Prepended to the auto-generated "given a request, decide which specialists to engage" instructions. */
|
|
20
|
+
systemPromptPrefix?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds a gatherClaims()-compatible function that first asks an LLM which
|
|
24
|
+
* registered specialists are relevant to this specific request, then runs
|
|
25
|
+
* only those specialists and merges their claims. Drop the result straight
|
|
26
|
+
* into runTrustedJob's `gatherClaims` parameter -- this replaces manually
|
|
27
|
+
* calling combineClaimGatherers() with every specialist unconditionally.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createLangChainOrchestrator(options: LangChainOrchestratorOptions): (context: Record<string, unknown>) => Promise<Claim[]>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator layer: an LLM decides WHICH of your registered specialists
|
|
3
|
+
* a specific request actually needs, then only those specialists' own
|
|
4
|
+
* gatherClaims() functions run. This is the piece createLangChainClaimGatherer
|
|
5
|
+
* alone doesn't provide -- that wraps ONE specialist; this is the dynamic
|
|
6
|
+
* "orchestrator assigns specialists" step sitting above it, generalized
|
|
7
|
+
* from the reference apps' langchainPlanner.ts (planSpecialists()).
|
|
8
|
+
*
|
|
9
|
+
* Full picture, matching the reference apps' architecture exactly:
|
|
10
|
+
* orchestrator (this file) -- decides which specialists to engage
|
|
11
|
+
* specialist (createLangChainClaimGatherer) -- checks and drafts claims
|
|
12
|
+
* evaluator (@arcproof/sdk's VerifierRegistry) -- independently verifies
|
|
13
|
+
*
|
|
14
|
+
* Deliberately no fallback that hides a genuine planning failure: if the
|
|
15
|
+
* structured-output call itself throws, this lets it propagate --
|
|
16
|
+
* runTrustedJob's existing catch block already refunds the locked budget
|
|
17
|
+
* rather than silently engaging every specialist and hoping for the best.
|
|
18
|
+
* The one defensive guard kept from the reference apps is different in
|
|
19
|
+
* kind: if the call SUCCEEDS but returns zero valid specialist ids (a
|
|
20
|
+
* degenerate answer, not a failure), default to engaging all of them --
|
|
21
|
+
* matching langchainPlanner.ts's `if (!plan.specialist_ids.length) ...`
|
|
22
|
+
* line exactly.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
function planSchema(specialistIds) {
|
|
26
|
+
return z.object({
|
|
27
|
+
specialist_ids: z
|
|
28
|
+
.array(z.enum(specialistIds))
|
|
29
|
+
.describe("Subset of the available specialists actually relevant to this specific request -- each one costs the requester's budget"),
|
|
30
|
+
reasoning: z.string().describe("One sentence on why these specialists (and not others) answer the request"),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds a gatherClaims()-compatible function that first asks an LLM which
|
|
35
|
+
* registered specialists are relevant to this specific request, then runs
|
|
36
|
+
* only those specialists and merges their claims. Drop the result straight
|
|
37
|
+
* into runTrustedJob's `gatherClaims` parameter -- this replaces manually
|
|
38
|
+
* calling combineClaimGatherers() with every specialist unconditionally.
|
|
39
|
+
*/
|
|
40
|
+
export function createLangChainOrchestrator(options) {
|
|
41
|
+
const specialistIds = options.specialists.map((s) => s.id);
|
|
42
|
+
if (specialistIds.length === 0)
|
|
43
|
+
throw new Error("createLangChainOrchestrator: at least one specialist is required");
|
|
44
|
+
const schema = planSchema(specialistIds);
|
|
45
|
+
return async function gatherClaims(context) {
|
|
46
|
+
const systemPrompt = (options.systemPromptPrefix ?? "You are the orchestrator for a bonded, independently-verified agent network. ") +
|
|
47
|
+
"Given a request, decide which specialist agents to engage. Each specialist costs the requester's budget, " +
|
|
48
|
+
"so only pick ones actually relevant to the request:\n\n" +
|
|
49
|
+
options.specialists.map((s) => `- ${s.id}: ${s.description}`).join("\n");
|
|
50
|
+
const userMsg = options.buildPlanningMessage
|
|
51
|
+
? options.buildPlanningMessage(context)
|
|
52
|
+
: (context.requestText ?? JSON.stringify(context));
|
|
53
|
+
// No try/catch here on purpose -- a genuine planning failure should
|
|
54
|
+
// propagate to runTrustedJob's refund path, not be papered over.
|
|
55
|
+
const structured = options.model.withStructuredOutput(schema, { name: "OrchestratorPlan" });
|
|
56
|
+
const plan = await structured.invoke([
|
|
57
|
+
{ role: "system", content: systemPrompt },
|
|
58
|
+
{ role: "user", content: userMsg },
|
|
59
|
+
]);
|
|
60
|
+
let selectedIds = plan.specialist_ids.filter((id) => specialistIds.includes(id));
|
|
61
|
+
if (!selectedIds.length)
|
|
62
|
+
selectedIds = specialistIds; // degenerate-but-successful answer -- engage everyone rather than nobody
|
|
63
|
+
console.log(`[orchestrator] plan: [${selectedIds.join(", ")}] -- ${plan.reasoning}`);
|
|
64
|
+
const chosen = options.specialists.filter((s) => selectedIds.includes(s.id));
|
|
65
|
+
const results = await Promise.all(chosen.map((s) => s.gatherClaims(context).catch((e) => {
|
|
66
|
+
console.log(`[orchestrator] ! ${s.id} threw (${e}) -- treating as zero claims from it`);
|
|
67
|
+
return [];
|
|
68
|
+
})));
|
|
69
|
+
return results.flat();
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../src/orchestrator.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcproof/sdk-langchain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LangChain.js adapter for @arcproof/sdk -- turn any LangChain.js tool-calling agent into a claim-gathering step for the ArcProof trust layer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist", "README.md"],
|
|
15
|
+
"publishConfig": { "access": "public" },
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["arcproof", "langchain", "ai-agents", "verification"],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@arcproof/sdk": "^0.1.0",
|
|
24
|
+
"@langchain/core": "^0.3.30",
|
|
25
|
+
"@langchain/langgraph": "^0.2.41"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"zod": "^3.24.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@arcproof/sdk": "*",
|
|
32
|
+
"@langchain/core": "^0.3.30",
|
|
33
|
+
"@langchain/langgraph": "^0.2.41",
|
|
34
|
+
"typescript": "^5.7.3",
|
|
35
|
+
"@types/node": "^22.10.5"
|
|
36
|
+
}
|
|
37
|
+
}
|