@xache/langchain 0.2.3 → 0.4.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/src/graph.ts ADDED
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Xache Knowledge Graph for LangChain.js
3
+ * Extract, query, and manage entities/relationships in a privacy-preserving knowledge graph
4
+ */
5
+
6
+ import { DynamicStructuredTool } from '@langchain/core/tools';
7
+ import { BaseRetriever, BaseRetrieverInput } from '@langchain/core/retrievers';
8
+ import { Document } from '@langchain/core/documents';
9
+ import { CallbackManagerForRetrieverRun } from '@langchain/core/callbacks/manager';
10
+ import { z } from 'zod';
11
+ import {
12
+ XacheClient,
13
+ DID,
14
+ type LLMProvider,
15
+ type LLMApiFormat,
16
+ type SubjectContext,
17
+ } from '@xache/sdk';
18
+
19
+ // =============================================================================
20
+ // Configuration
21
+ // =============================================================================
22
+
23
+ export interface GraphToolConfig {
24
+ /** Wallet address for authentication */
25
+ walletAddress: string;
26
+ /** Private key for signing */
27
+ privateKey: string;
28
+ /** API URL (defaults to https://api.xache.xyz) */
29
+ apiUrl?: string;
30
+ /** Chain: 'base' or 'solana' */
31
+ chain?: 'base' | 'solana';
32
+ /** Subject context for graph scoping (defaults to GLOBAL) */
33
+ subject?: SubjectContext;
34
+ /** LLM provider for extract/ask (api-key mode) */
35
+ llmProvider?: LLMProvider;
36
+ /** LLM API key */
37
+ llmApiKey?: string;
38
+ /** LLM model override */
39
+ llmModel?: string;
40
+ /** LLM endpoint URL (endpoint mode) */
41
+ llmEndpoint?: string;
42
+ /** LLM auth token (endpoint mode) */
43
+ llmAuthToken?: string;
44
+ /** LLM API format (endpoint mode, default: openai) */
45
+ llmFormat?: LLMApiFormat;
46
+ }
47
+
48
+ function createClient(config: GraphToolConfig): XacheClient {
49
+ const chainPrefix = config.chain === 'solana' ? 'sol' : 'evm';
50
+ const did = `did:agent:${chainPrefix}:${config.walletAddress.toLowerCase()}` as DID;
51
+
52
+ return new XacheClient({
53
+ apiUrl: config.apiUrl || 'https://api.xache.xyz',
54
+ did,
55
+ privateKey: config.privateKey,
56
+ });
57
+ }
58
+
59
+ function getSubject(config: GraphToolConfig): SubjectContext {
60
+ return config.subject || { scope: 'GLOBAL' };
61
+ }
62
+
63
+ function buildLLMConfig(config: GraphToolConfig): Record<string, unknown> {
64
+ if (config.llmEndpoint) {
65
+ return {
66
+ type: 'endpoint' as const,
67
+ url: config.llmEndpoint,
68
+ authToken: config.llmAuthToken,
69
+ format: config.llmFormat || 'openai',
70
+ model: config.llmModel,
71
+ };
72
+ } else if (config.llmApiKey && config.llmProvider) {
73
+ return {
74
+ type: 'api-key' as const,
75
+ provider: config.llmProvider,
76
+ apiKey: config.llmApiKey,
77
+ model: config.llmModel,
78
+ };
79
+ } else {
80
+ return {
81
+ type: 'xache-managed' as const,
82
+ provider: 'anthropic',
83
+ model: config.llmModel,
84
+ };
85
+ }
86
+ }
87
+
88
+ // =============================================================================
89
+ // Factory Functions
90
+ // =============================================================================
91
+
92
+ /**
93
+ * Create a tool for extracting entities/relationships from agent traces.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const extractTool = createGraphExtractTool({
98
+ * walletAddress: '0x...',
99
+ * privateKey: '0x...',
100
+ * llmProvider: 'anthropic',
101
+ * llmApiKey: 'sk-ant-...',
102
+ * });
103
+ * ```
104
+ */
105
+ export function createGraphExtractTool(
106
+ config: GraphToolConfig
107
+ ): DynamicStructuredTool {
108
+ const client = createClient(config);
109
+ const subject = getSubject(config);
110
+
111
+ return new DynamicStructuredTool({
112
+ name: 'xache_graph_extract',
113
+ description:
114
+ 'Extract entities and relationships from text into the knowledge graph. ' +
115
+ 'Identifies people, organizations, tools, concepts and their connections.',
116
+ schema: z.object({
117
+ trace: z.string().describe('The text/trace to extract entities from'),
118
+ contextHint: z.string().optional().describe('Domain hint (e.g., "engineering", "customer-support")'),
119
+ }),
120
+ func: async ({ trace, contextHint }) => {
121
+ const result = await client.graph.extract({
122
+ trace,
123
+ llmConfig: buildLLMConfig(config) as any,
124
+ subject,
125
+ options: {
126
+ contextHint,
127
+ confidenceThreshold: 0.7,
128
+ },
129
+ });
130
+
131
+ const entities = result.entities || [];
132
+ const relationships = result.relationships || [];
133
+
134
+ if (entities.length === 0 && relationships.length === 0) {
135
+ return 'No entities or relationships extracted.';
136
+ }
137
+
138
+ let output = `Extracted ${entities.length} entities, ${relationships.length} relationships.\n`;
139
+ for (const e of entities) {
140
+ output += ` Entity: ${e.name} [${e.type}]${e.isNew ? ' (new)' : ''}\n`;
141
+ }
142
+ for (const r of relationships) {
143
+ output += ` Rel: ${r.from} → ${r.type} → ${r.to}\n`;
144
+ }
145
+ return output;
146
+ },
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Create a tool for querying the knowledge graph around a specific entity.
152
+ */
153
+ export function createGraphQueryTool(
154
+ config: GraphToolConfig
155
+ ): DynamicStructuredTool {
156
+ const client = createClient(config);
157
+ const subject = getSubject(config);
158
+
159
+ return new DynamicStructuredTool({
160
+ name: 'xache_graph_query',
161
+ description:
162
+ 'Query the knowledge graph around a specific entity. ' +
163
+ 'Returns connected entities and relationships within the specified depth.',
164
+ schema: z.object({
165
+ startEntity: z.string().describe('Name of the entity to start from'),
166
+ depth: z.number().optional().default(2).describe('Number of hops (default: 2)'),
167
+ }),
168
+ func: async ({ startEntity, depth }) => {
169
+ const graph = await client.graph.query({
170
+ subject,
171
+ startEntity,
172
+ depth: depth || 2,
173
+ });
174
+
175
+ const data = graph.toJSON();
176
+ if (data.entities.length === 0) {
177
+ return `No entities found connected to "${startEntity}".`;
178
+ }
179
+
180
+ let output = `Subgraph: ${data.entities.length} entities, ${data.relationships.length} relationships\n`;
181
+ for (const e of data.entities) {
182
+ output += ` ${e.name} [${e.type}]`;
183
+ if (e.summary) output += ` — ${e.summary.substring(0, 80)}`;
184
+ output += '\n';
185
+ }
186
+ return output;
187
+ },
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Create a tool for asking natural language questions about the knowledge graph.
193
+ */
194
+ export function createGraphAskTool(
195
+ config: GraphToolConfig
196
+ ): DynamicStructuredTool {
197
+ const client = createClient(config);
198
+ const subject = getSubject(config);
199
+
200
+ return new DynamicStructuredTool({
201
+ name: 'xache_graph_ask',
202
+ description:
203
+ 'Ask a natural language question about the knowledge graph. ' +
204
+ 'Uses LLM to analyze entities and relationships and provide an answer.',
205
+ schema: z.object({
206
+ question: z.string().describe('The question to ask about the knowledge graph'),
207
+ }),
208
+ func: async ({ question }) => {
209
+ const answer = await client.graph.ask({
210
+ subject,
211
+ question,
212
+ llmConfig: buildLLMConfig(config) as any,
213
+ });
214
+
215
+ let output = `Answer: ${answer.answer}\nConfidence: ${(answer.confidence * 100).toFixed(0)}%`;
216
+ if (answer.sources?.length > 0) {
217
+ output += '\nSources: ' + answer.sources.map(s => `${s.name} [${s.type}]`).join(', ');
218
+ }
219
+ return output;
220
+ },
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Create a tool for adding entities to the knowledge graph.
226
+ */
227
+ export function createGraphAddEntityTool(
228
+ config: GraphToolConfig
229
+ ): DynamicStructuredTool {
230
+ const client = createClient(config);
231
+ const subject = getSubject(config);
232
+
233
+ return new DynamicStructuredTool({
234
+ name: 'xache_graph_add_entity',
235
+ description:
236
+ 'Add an entity to the knowledge graph. ' +
237
+ 'Creates a person, organization, tool, concept, or other entity type.',
238
+ schema: z.object({
239
+ name: z.string().describe('Entity name'),
240
+ type: z.string().describe('Entity type (person, organization, tool, concept, etc.)'),
241
+ summary: z.string().optional().describe('Brief description'),
242
+ attributes: z.record(z.string(), z.unknown()).optional().describe('Key-value attributes'),
243
+ }),
244
+ func: async ({ name, type, summary, attributes }) => {
245
+ const entity = await client.graph.addEntity({
246
+ subject,
247
+ name,
248
+ type,
249
+ summary,
250
+ attributes,
251
+ });
252
+ return `Created entity "${entity.name}" [${entity.type}], key: ${entity.key}`;
253
+ },
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Create a tool for adding relationships between entities.
259
+ */
260
+ export function createGraphAddRelationshipTool(
261
+ config: GraphToolConfig
262
+ ): DynamicStructuredTool {
263
+ const client = createClient(config);
264
+ const subject = getSubject(config);
265
+
266
+ return new DynamicStructuredTool({
267
+ name: 'xache_graph_add_relationship',
268
+ description:
269
+ 'Create a relationship between two entities in the knowledge graph.',
270
+ schema: z.object({
271
+ from: z.string().describe('Source entity name'),
272
+ to: z.string().describe('Target entity name'),
273
+ type: z.string().describe('Relationship type (works_at, knows, uses, manages, etc.)'),
274
+ description: z.string().optional().describe('Relationship description'),
275
+ }),
276
+ func: async ({ from, to, type, description }) => {
277
+ const rel = await client.graph.addRelationship({
278
+ subject,
279
+ from,
280
+ to,
281
+ type,
282
+ description,
283
+ });
284
+ return `Created relationship: ${from} → ${rel.type} → ${to}`;
285
+ },
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Create a tool for loading the full knowledge graph.
291
+ */
292
+ export function createGraphLoadTool(
293
+ config: GraphToolConfig
294
+ ): DynamicStructuredTool {
295
+ const client = createClient(config);
296
+ const subject = getSubject(config);
297
+
298
+ return new DynamicStructuredTool({
299
+ name: 'xache_graph_load',
300
+ description:
301
+ 'Load the full knowledge graph. Returns all entities and relationships. ' +
302
+ 'Optionally filter by entity type or load a historical snapshot.',
303
+ schema: z.object({
304
+ entityTypes: z.array(z.string()).optional().describe('Filter to specific entity types'),
305
+ validAt: z.string().optional().describe('Load graph as it existed at this time (ISO8601)'),
306
+ }),
307
+ func: async ({ entityTypes, validAt }) => {
308
+ const graph = await client.graph.load({
309
+ subject,
310
+ entityTypes,
311
+ validAt,
312
+ });
313
+
314
+ const data = graph.toJSON();
315
+ if (data.entities.length === 0) {
316
+ return 'Knowledge graph is empty.';
317
+ }
318
+
319
+ let output = `Knowledge graph: ${data.entities.length} entities, ${data.relationships.length} relationships\n`;
320
+ for (const e of data.entities) {
321
+ output += ` ${e.name} [${e.type}]`;
322
+ if (e.summary) output += ` — ${e.summary.substring(0, 80)}`;
323
+ output += '\n';
324
+ }
325
+ return output;
326
+ },
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Create a tool for merging two entities in the knowledge graph.
332
+ */
333
+ export function createGraphMergeEntitiesTool(
334
+ config: GraphToolConfig
335
+ ): DynamicStructuredTool {
336
+ const client = createClient(config);
337
+ const subject = getSubject(config);
338
+
339
+ return new DynamicStructuredTool({
340
+ name: 'xache_graph_merge_entities',
341
+ description:
342
+ 'Merge two entities into one. The source entity is superseded and the target ' +
343
+ 'entity is updated with merged attributes. Relationships are transferred.',
344
+ schema: z.object({
345
+ sourceName: z.string().describe('Entity to merge FROM (will be superseded)'),
346
+ targetName: z.string().describe('Entity to merge INTO (will be updated)'),
347
+ }),
348
+ func: async ({ sourceName, targetName }) => {
349
+ const merged = await client.graph.mergeEntities({
350
+ subject,
351
+ sourceName,
352
+ targetName,
353
+ });
354
+ return `Merged "${sourceName}" into "${targetName}". Result: ${merged.name} [${merged.type}] (v${merged.version})`;
355
+ },
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Create a tool for getting the version history of an entity.
361
+ */
362
+ export function createGraphEntityHistoryTool(
363
+ config: GraphToolConfig
364
+ ): DynamicStructuredTool {
365
+ const client = createClient(config);
366
+ const subject = getSubject(config);
367
+
368
+ return new DynamicStructuredTool({
369
+ name: 'xache_graph_entity_history',
370
+ description:
371
+ 'Get the full version history of an entity. Shows how the entity has changed over time.',
372
+ schema: z.object({
373
+ name: z.string().describe('Entity name to look up history for'),
374
+ }),
375
+ func: async ({ name }) => {
376
+ const versions = await client.graph.getEntityHistory({
377
+ subject,
378
+ name,
379
+ });
380
+
381
+ if (versions.length === 0) {
382
+ return `No history found for entity "${name}".`;
383
+ }
384
+
385
+ let output = `History for "${name}": ${versions.length} version(s)\n`;
386
+ for (const v of versions) {
387
+ output += ` v${v.version} — ${v.name} [${v.type}]`;
388
+ if (v.summary) output += ` | ${v.summary.substring(0, 80)}`;
389
+ output += `\n Valid: ${v.validFrom}${v.validTo ? ` → ${v.validTo}` : ' → current'}\n`;
390
+ }
391
+ return output;
392
+ },
393
+ });
394
+ }
395
+
396
+ // =============================================================================
397
+ // Class Wrappers
398
+ // =============================================================================
399
+
400
+ /**
401
+ * Class wrapper for graph extract tool
402
+ */
403
+ export class XacheGraphExtractTool {
404
+ private tool: DynamicStructuredTool;
405
+
406
+ constructor(config: GraphToolConfig) {
407
+ this.tool = createGraphExtractTool(config);
408
+ }
409
+
410
+ asTool(): DynamicStructuredTool {
411
+ return this.tool;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Class wrapper for graph query tool
417
+ */
418
+ export class XacheGraphQueryTool {
419
+ private tool: DynamicStructuredTool;
420
+
421
+ constructor(config: GraphToolConfig) {
422
+ this.tool = createGraphQueryTool(config);
423
+ }
424
+
425
+ asTool(): DynamicStructuredTool {
426
+ return this.tool;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Class wrapper for graph ask tool
432
+ */
433
+ export class XacheGraphAskTool {
434
+ private tool: DynamicStructuredTool;
435
+
436
+ constructor(config: GraphToolConfig) {
437
+ this.tool = createGraphAskTool(config);
438
+ }
439
+
440
+ asTool(): DynamicStructuredTool {
441
+ return this.tool;
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Class wrapper for graph load tool
447
+ */
448
+ export class XacheGraphLoadTool {
449
+ private tool: DynamicStructuredTool;
450
+
451
+ constructor(config: GraphToolConfig) {
452
+ this.tool = createGraphLoadTool(config);
453
+ }
454
+
455
+ asTool(): DynamicStructuredTool {
456
+ return this.tool;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Class wrapper for graph merge entities tool
462
+ */
463
+ export class XacheGraphMergeEntitiesTool {
464
+ private tool: DynamicStructuredTool;
465
+
466
+ constructor(config: GraphToolConfig) {
467
+ this.tool = createGraphMergeEntitiesTool(config);
468
+ }
469
+
470
+ asTool(): DynamicStructuredTool {
471
+ return this.tool;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Class wrapper for graph entity history tool
477
+ */
478
+ export class XacheGraphEntityHistoryTool {
479
+ private tool: DynamicStructuredTool;
480
+
481
+ constructor(config: GraphToolConfig) {
482
+ this.tool = createGraphEntityHistoryTool(config);
483
+ }
484
+
485
+ asTool(): DynamicStructuredTool {
486
+ return this.tool;
487
+ }
488
+ }
489
+
490
+ // =============================================================================
491
+ // Graph Retriever
492
+ // =============================================================================
493
+
494
+ export interface XacheGraphRetrieverConfig extends BaseRetrieverInput {
495
+ /** Wallet address for authentication */
496
+ walletAddress: string;
497
+ /** Private key for signing */
498
+ privateKey: string;
499
+ /** API URL (defaults to https://api.xache.xyz) */
500
+ apiUrl?: string;
501
+ /** Chain: 'base' or 'solana' */
502
+ chain?: 'base' | 'solana';
503
+ /** Subject context for graph scoping */
504
+ subject?: SubjectContext;
505
+ /** Starting entity for subgraph query (optional — if not set, loads full graph) */
506
+ startEntity?: string;
507
+ /** Depth for subgraph query (default: 2) */
508
+ depth?: number;
509
+ /** Max results to return */
510
+ k?: number;
511
+ }
512
+
513
+ /**
514
+ * Retriever that fetches documents from the Xache knowledge graph.
515
+ * Each entity becomes a Document with its name, type, and summary.
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * const retriever = new XacheGraphRetriever({
520
+ * walletAddress: '0x...',
521
+ * privateKey: '0x...',
522
+ * k: 10,
523
+ * });
524
+ *
525
+ * const docs = await retriever.getRelevantDocuments('engineering team');
526
+ * ```
527
+ */
528
+ export class XacheGraphRetriever extends BaseRetriever {
529
+ lc_namespace = ['xache', 'graph_retriever'];
530
+
531
+ static lc_name() {
532
+ return 'XacheGraphRetriever';
533
+ }
534
+
535
+ private client: XacheClient;
536
+ private subject: SubjectContext;
537
+ private startEntity?: string;
538
+ private depth: number;
539
+ private k: number;
540
+
541
+ constructor(config: XacheGraphRetrieverConfig) {
542
+ super(config);
543
+
544
+ const chainPrefix = config.chain === 'solana' ? 'sol' : 'evm';
545
+ const did = `did:agent:${chainPrefix}:${config.walletAddress.toLowerCase()}` as DID;
546
+
547
+ this.client = new XacheClient({
548
+ apiUrl: config.apiUrl || 'https://api.xache.xyz',
549
+ did,
550
+ privateKey: config.privateKey,
551
+ });
552
+
553
+ this.subject = config.subject || { scope: 'GLOBAL' };
554
+ this.startEntity = config.startEntity;
555
+ this.depth = config.depth ?? 2;
556
+ this.k = config.k ?? 10;
557
+ }
558
+
559
+ async _getRelevantDocuments(
560
+ query: string,
561
+ _runManager?: CallbackManagerForRetrieverRun,
562
+ ): Promise<Document[]> {
563
+ // Load graph (full or subgraph)
564
+ let graph;
565
+ if (this.startEntity) {
566
+ graph = await this.client.graph.query({
567
+ subject: this.subject,
568
+ startEntity: this.startEntity,
569
+ depth: this.depth,
570
+ });
571
+ } else {
572
+ graph = await this.client.graph.load({ subject: this.subject });
573
+ }
574
+
575
+ const data = graph.toJSON();
576
+ const queryLower = query.toLowerCase();
577
+
578
+ // Score entities by relevance to query
579
+ const scored = data.entities.map(entity => {
580
+ let score = 0;
581
+ const nameLower = entity.name.toLowerCase();
582
+ const summaryLower = (entity.summary || '').toLowerCase();
583
+
584
+ if (nameLower.includes(queryLower)) score += 3;
585
+ if (summaryLower.includes(queryLower)) score += 2;
586
+
587
+ // Check individual query terms
588
+ const terms = queryLower.split(/\s+/);
589
+ for (const term of terms) {
590
+ if (nameLower.includes(term)) score += 1;
591
+ if (summaryLower.includes(term)) score += 0.5;
592
+ }
593
+
594
+ return { entity, score };
595
+ });
596
+
597
+ // Sort by score, take top k
598
+ scored.sort((a, b) => b.score - a.score);
599
+ const topEntities = scored.slice(0, this.k);
600
+
601
+ return topEntities.map(({ entity }) => {
602
+ const content = [
603
+ `Name: ${entity.name}`,
604
+ `Type: ${entity.type}`,
605
+ entity.summary ? `Summary: ${entity.summary}` : null,
606
+ ].filter(Boolean).join('\n');
607
+
608
+ return new Document({
609
+ pageContent: content,
610
+ metadata: {
611
+ source: 'xache-graph',
612
+ entityKey: entity.key,
613
+ entityName: entity.name,
614
+ entityType: entity.type,
615
+ storageKey: entity.storageKey,
616
+ },
617
+ });
618
+ });
619
+ }
620
+ }
package/src/index.ts CHANGED
@@ -57,3 +57,23 @@ export {
57
57
  XacheReputationChecker,
58
58
  } from './reputation';
59
59
  export type { ReputationToolConfig, ReputationResult } from './reputation';
60
+
61
+ // Knowledge Graph
62
+ export {
63
+ createGraphExtractTool,
64
+ createGraphLoadTool,
65
+ createGraphQueryTool,
66
+ createGraphAskTool,
67
+ createGraphAddEntityTool,
68
+ createGraphAddRelationshipTool,
69
+ createGraphMergeEntitiesTool,
70
+ createGraphEntityHistoryTool,
71
+ XacheGraphExtractTool,
72
+ XacheGraphLoadTool,
73
+ XacheGraphQueryTool,
74
+ XacheGraphAskTool,
75
+ XacheGraphMergeEntitiesTool,
76
+ XacheGraphEntityHistoryTool,
77
+ XacheGraphRetriever,
78
+ } from './graph';
79
+ export type { GraphToolConfig, XacheGraphRetrieverConfig } from './graph';