@exulu/backend 1.55.0 → 1.56.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.
@@ -0,0 +1,79 @@
1
+ import { ExuluContext, getTableName } from "@SRC/exulu/context";
2
+ import { postgresClient } from "@SRC/postgres/client";
3
+ import { applyAccessControl } from "@SRC/graphql/utilities/access-control";
4
+ import { convertContextToTableDefinition } from "@SRC/graphql/utilities/convert-context-to-table-definition";
5
+ import type { User } from "@EXULU_TYPES/models/user";
6
+
7
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
8
+
9
+ export interface ContextSample {
10
+ contextId: string;
11
+ contextName: string;
12
+ /** All field names available on items (standard + custom) */
13
+ fields: string[];
14
+ /** Up to 2 example item records */
15
+ exampleItems: Array<Record<string, any>>;
16
+ sampledAt: number;
17
+ }
18
+
19
+ /**
20
+ * Pulls 1–2 example item records per context at agent initialization and caches
21
+ * them in memory. These samples are injected into the classifier prompt so the
22
+ * model understands what data is actually stored (not just field names).
23
+ */
24
+ export class ContextSampler {
25
+ private cache = new Map<string, ContextSample>();
26
+
27
+ async getSamples(
28
+ contexts: ExuluContext[],
29
+ user?: User,
30
+ role?: string,
31
+ ): Promise<ContextSample[]> {
32
+ return Promise.all(contexts.map((ctx) => this.getSample(ctx, user, role)));
33
+ }
34
+
35
+ private async getSample(
36
+ ctx: ExuluContext,
37
+ user?: User,
38
+ role?: string,
39
+ ): Promise<ContextSample> {
40
+ const cached = this.cache.get(ctx.id);
41
+ if (cached && Date.now() - cached.sampledAt < CACHE_TTL_MS) {
42
+ return cached;
43
+ }
44
+
45
+ const { db } = await postgresClient();
46
+ const tableName = getTableName(ctx.id);
47
+ const tableDefinition = convertContextToTableDefinition(ctx);
48
+
49
+ const customFieldNames = ctx.fields.map((f) => f.name);
50
+ const selectFields = ["id", "name", "external_id", ...customFieldNames];
51
+
52
+ let exampleItems: Record<string, any>[] = [];
53
+ try {
54
+ let query = db(tableName).select(selectFields).whereNull("archived").limit(2);
55
+ query = applyAccessControl(tableDefinition, query, user, tableName);
56
+ exampleItems = await query;
57
+ } catch {
58
+ // If table doesn't exist yet or column mismatch, return empty samples
59
+ }
60
+
61
+ const sample: ContextSample = {
62
+ contextId: ctx.id,
63
+ contextName: ctx.name,
64
+ fields: ["name", "external_id", ...customFieldNames],
65
+ exampleItems,
66
+ sampledAt: Date.now(),
67
+ };
68
+
69
+ this.cache.set(ctx.id, sample);
70
+
71
+ // Refresh in background after TTL without blocking the caller
72
+ return sample;
73
+ }
74
+
75
+ /** Evict a context from cache so it's re-sampled on next use */
76
+ invalidate(contextId: string): void {
77
+ this.cache.delete(contextId);
78
+ }
79
+ }