@exulu/backend 3.6.0 → 3.7.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.
@@ -96,6 +96,9 @@ var spawnLiteLLM = (cfg) => {
96
96
  ...rest,
97
97
  DEBUG: "false"
98
98
  };
99
+ if (!process.env.DISABLE_SCHEMA_UPDATE) {
100
+ childEnv.DISABLE_SCHEMA_UPDATE = "true";
101
+ }
99
102
  if (LITELLM_UI_PATH && !process.env.SERVER_ROOT_PATH) {
100
103
  childEnv.SERVER_ROOT_PATH = LITELLM_UI_PATH;
101
104
  }
@@ -1713,7 +1716,7 @@ var ExuluTool = class _ExuluTool {
1713
1716
  if (!agent) {
1714
1717
  throw new Error("Agent not found.");
1715
1718
  }
1716
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-TZ2UKWR4.js");
1719
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-UQSLJDXE.js");
1717
1720
  const tools = await convertExuluToolsToAiSdkTools2(
1718
1721
  [this],
1719
1722
  [],
@@ -8183,5 +8186,6 @@ export {
8183
8186
  ExuluTool,
8184
8187
  resolveReranker,
8185
8188
  withRetry,
8189
+ microCallProviderOptions,
8186
8190
  createAgenticRetrievalTool
8187
8191
  };
@@ -2,7 +2,7 @@ import "dotenv/config";
2
2
  import {
3
3
  convertExuluToolsToAiSdkTools,
4
4
  hydrateVariables
5
- } from "./chunk-7AZH4ETH.js";
5
+ } from "./chunk-BNTL6LYY.js";
6
6
  import "./chunk-4PDWNVNT.js";
7
7
  export {
8
8
  convertExuluToolsToAiSdkTools,
package/dist/index.cjs CHANGED
@@ -1347,6 +1347,9 @@ var init_supervisor = __esm({
1347
1347
  ...rest,
1348
1348
  DEBUG: "false"
1349
1349
  };
1350
+ if (!process.env.DISABLE_SCHEMA_UPDATE) {
1351
+ childEnv.DISABLE_SCHEMA_UPDATE = "true";
1352
+ }
1350
1353
  if (LITELLM_UI_PATH && !process.env.SERVER_ROOT_PATH) {
1351
1354
  childEnv.SERVER_ROOT_PATH = LITELLM_UI_PATH;
1352
1355
  }
@@ -8892,10 +8895,12 @@ __export(index_exports, {
8892
8895
  ExuluDocumentProcessor: () => ExuluDocumentProcessor,
8893
8896
  ExuluEval: () => ExuluEval,
8894
8897
  ExuluJobs: () => ExuluJobs,
8898
+ ExuluModels: () => ExuluModels,
8895
8899
  ExuluOtel: () => ExuluOtel,
8896
8900
  ExuluPython: () => ExuluPython,
8897
8901
  ExuluQueues: () => queues,
8898
8902
  ExuluReadApi: () => ExuluReadApi,
8903
+ ExuluRecall: () => ExuluRecall,
8899
8904
  ExuluReranker: () => ExuluReranker,
8900
8905
  ExuluTool: () => ExuluTool,
8901
8906
  ExuluVariables: () => ExuluVariables,
@@ -13714,7 +13719,7 @@ var ExuluContext2 = class {
13714
13719
  }
13715
13720
  console.log("[EXULU] context configuration", this.configuration);
13716
13721
  let jobs = [];
13717
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
13722
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
13718
13723
  if (this.processor) {
13719
13724
  const processor = this.processor;
13720
13725
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13736,12 +13741,12 @@ var ExuluContext2 = class {
13736
13741
  ...processorResult
13737
13742
  });
13738
13743
  if (processor.config?.generateEmbeddings) {
13739
- shouldGenerateEmbeddings = true;
13744
+ shouldGenerateEmbeddings2 = true;
13740
13745
  }
13741
13746
  }
13742
13747
  }
13743
13748
  }
13744
- if (shouldGenerateEmbeddings) {
13749
+ if (shouldGenerateEmbeddings2) {
13745
13750
  console.log("[EXULU] generating embeddings for item", results[0].id);
13746
13751
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13747
13752
  item: {
@@ -13785,7 +13790,7 @@ var ExuluContext2 = class {
13785
13790
  }).returning("id");
13786
13791
  await mutation;
13787
13792
  let jobs = [];
13788
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
13793
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
13789
13794
  if (this.processor) {
13790
13795
  const processor = this.processor;
13791
13796
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13807,12 +13812,12 @@ var ExuluContext2 = class {
13807
13812
  ...processorResult
13808
13813
  });
13809
13814
  if (processor.config?.generateEmbeddings) {
13810
- shouldGenerateEmbeddings = true;
13815
+ shouldGenerateEmbeddings2 = true;
13811
13816
  }
13812
13817
  }
13813
13818
  }
13814
13819
  }
13815
- if (shouldGenerateEmbeddings) {
13820
+ if (shouldGenerateEmbeddings2) {
13816
13821
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13817
13822
  item: record,
13818
13823
  // important we need to full record here with all fields for the embedder
@@ -15668,6 +15673,18 @@ var applyAgentGuestFieldTransforms = async (input) => {
15668
15673
  return input;
15669
15674
  };
15670
15675
 
15676
+ // src/graphql/mutations/should-generate-embeddings.ts
15677
+ init_cjs_shims();
15678
+ var shouldGenerateEmbeddings = ({
15679
+ calculateVectors,
15680
+ operation,
15681
+ override
15682
+ }) => {
15683
+ if (override !== void 0) return override;
15684
+ if (calculateVectors === "always") return true;
15685
+ return operation === "create" ? calculateVectors === "onInsert" : calculateVectors === "onUpdate";
15686
+ };
15687
+
15671
15688
  // src/graphql/mutations/index.ts
15672
15689
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
15673
15690
  var postprocessDeletion = async ({
@@ -15736,7 +15753,9 @@ var postprocessUpdate = async ({
15736
15753
  result,
15737
15754
  user,
15738
15755
  role,
15739
- config
15756
+ config,
15757
+ operation,
15758
+ generateEmbeddings
15740
15759
  }) => {
15741
15760
  if (!result) {
15742
15761
  return result;
@@ -15763,7 +15782,11 @@ var postprocessUpdate = async ({
15763
15782
  if (!context.embedder) {
15764
15783
  return result;
15765
15784
  }
15766
- if (context.embedder && (context.configuration.calculateVectors === "onUpdate" || context.configuration.calculateVectors === "always")) {
15785
+ if (shouldGenerateEmbeddings({
15786
+ calculateVectors: context.configuration.calculateVectors,
15787
+ operation,
15788
+ override: generateEmbeddings
15789
+ })) {
15767
15790
  const { db: db2 } = await postgresClient();
15768
15791
  console.log("[EXULU] Deleting chunks for item", result.id);
15769
15792
  const exists = await context.chunksTableExists();
@@ -16013,7 +16036,9 @@ function createMutations(table, contexts, tools, config) {
16013
16036
  result: results[0],
16014
16037
  user: context.user.id,
16015
16038
  role: context.user.role?.id,
16016
- config
16039
+ config,
16040
+ operation: "create",
16041
+ generateEmbeddings: args.generateEmbeddings
16017
16042
  });
16018
16043
  return {
16019
16044
  // Filter result to only include requested fields
@@ -16087,7 +16112,9 @@ function createMutations(table, contexts, tools, config) {
16087
16112
  result,
16088
16113
  user: context.user.id,
16089
16114
  role: context.user.role?.id,
16090
- config
16115
+ config,
16116
+ operation: "update",
16117
+ generateEmbeddings: args.generateEmbeddings
16091
16118
  });
16092
16119
  return {
16093
16120
  item: finalizeRequestedFields({
@@ -16151,7 +16178,9 @@ function createMutations(table, contexts, tools, config) {
16151
16178
  result,
16152
16179
  user: context.user.id,
16153
16180
  role: context.user.role?.id,
16154
- config
16181
+ config,
16182
+ operation: "update",
16183
+ generateEmbeddings: args.generateEmbeddings
16155
16184
  });
16156
16185
  return {
16157
16186
  item: finalizeRequestedFields({
@@ -16421,6 +16450,7 @@ function createMutations(table, contexts, tools, config) {
16421
16450
  };
16422
16451
  }
16423
16452
  query = applyFilters(query, args.where, table);
16453
+ query = applyAccessControl(table, query, context.user);
16424
16454
  if (args.limit) {
16425
16455
  query = query.limit(args.limit);
16426
16456
  }
@@ -19861,6 +19891,33 @@ var renderTranscript = (segments, speakers) => {
19861
19891
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
19862
19892
  };
19863
19893
 
19894
+ // src/exulu/transcription/build-transcript-item.ts
19895
+ init_cjs_shims();
19896
+ var buildTranscriptItemInput = ({
19897
+ row,
19898
+ title,
19899
+ speakers,
19900
+ transcriptText,
19901
+ rightsMode,
19902
+ isReSave
19903
+ }) => ({
19904
+ // Carrying the id on re-save makes context.createItem upsert in place.
19905
+ ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
19906
+ name: title ?? row.title ?? "Transcript",
19907
+ transcript_text: transcriptText,
19908
+ audio_s3key: row.audio_s3key,
19909
+ language: row.language ?? void 0,
19910
+ duration_seconds: row.duration_seconds ?? void 0,
19911
+ speakers,
19912
+ raw_segments: row.raw_segments,
19913
+ // Recall meeting-bot post-processing results (null for Whisper jobs).
19914
+ post_processing: row.post_processing_outputs ?? void 0,
19915
+ // Handle for the meeting video; null for Whisper uploads.
19916
+ recall_recording_id: row.recall_recording_id ?? void 0,
19917
+ rights_mode: rightsMode,
19918
+ created_by: row.created_by
19919
+ });
19920
+
19864
19921
  // src/exulu/transcription/service.ts
19865
19922
  var TABLE2 = "transcription_jobs";
19866
19923
  var log3 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -20053,21 +20110,14 @@ var transcriptionService = {
20053
20110
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
20054
20111
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
20055
20112
  const isReSave = row.status === "saved" && !!row.saved_item_id;
20056
- const itemInput = {
20057
- // Carrying the id on re-save makes context.createItem upsert in place.
20058
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
20059
- name: input.title ?? row.title ?? "Transcript",
20060
- transcript_text: transcriptText,
20061
- audio_s3key: row.audio_s3key,
20062
- language: row.language ?? void 0,
20063
- duration_seconds: row.duration_seconds ?? void 0,
20113
+ const itemInput = buildTranscriptItemInput({
20114
+ row,
20115
+ title: input.title,
20064
20116
  speakers: input.speakers,
20065
- raw_segments: row.raw_segments,
20066
- // Recall meeting-bot post-processing results (null for Whisper jobs).
20067
- post_processing: row.post_processing_outputs ?? void 0,
20068
- rights_mode: rightsMode,
20069
- created_by: row.created_by
20070
- };
20117
+ transcriptText,
20118
+ rightsMode,
20119
+ isReSave
20120
+ });
20071
20121
  let item;
20072
20122
  try {
20073
20123
  const result = await context.createItem(
@@ -20313,25 +20363,34 @@ var recordingDurationSeconds = (rec) => {
20313
20363
  }
20314
20364
  return null;
20315
20365
  };
20366
+ var RECORDING_RETENTION_HOURS = 2160;
20367
+ var buildCreateBotPayload = (input) => ({
20368
+ meeting_url: input.meeting_url,
20369
+ join_at: input.join_at,
20370
+ ...input.bot_name ? { bot_name: input.bot_name } : {},
20371
+ recording_config: {
20372
+ video_mixed_mp4: {},
20373
+ video_mixed_layout: "speaker_view",
20374
+ participant_events: {},
20375
+ meeting_metadata: {},
20376
+ retention: { type: "timed", hours: RECORDING_RETENTION_HOURS }
20377
+ },
20378
+ ...input.notifyChat ? {
20379
+ chat: {
20380
+ on_bot_join: {
20381
+ send_to: "everyone",
20382
+ message: input.notifyChat.message,
20383
+ pin: true
20384
+ }
20385
+ }
20386
+ } : {}
20387
+ });
20316
20388
  var recallClient = {
20317
20389
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
20318
20390
  /** POST /bot — schedule/launch a bot for a meeting. */
20319
20391
  createBot: (input) => request2("/bot/", {
20320
20392
  method: "POST",
20321
- body: JSON.stringify({
20322
- meeting_url: input.meeting_url,
20323
- join_at: input.join_at,
20324
- ...input.bot_name ? { bot_name: input.bot_name } : {},
20325
- ...input.notifyChat ? {
20326
- chat: {
20327
- on_bot_join: {
20328
- send_to: "everyone",
20329
- message: input.notifyChat.message,
20330
- pin: true
20331
- }
20332
- }
20333
- } : {}
20334
- })
20393
+ body: JSON.stringify(buildCreateBotPayload(input))
20335
20394
  }),
20336
20395
  /**
20337
20396
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -20647,6 +20706,28 @@ var recallService = {
20647
20706
  }
20648
20707
  return null;
20649
20708
  },
20709
+ /**
20710
+ * Fresh signed URL for a recording's mixed MP4, or null when there is no
20711
+ * usable video (still processing, no video artifact, recording expired or
20712
+ * deleted).
20713
+ *
20714
+ * Never cache the result: the URL carries X-Amz-Expires=21600, so it dies
20715
+ * after six hours. Resolve at point of use.
20716
+ */
20717
+ async getRecordingVideoUrl(recordingId) {
20718
+ try {
20719
+ const recording = await recallClient.retrieveRecording(recordingId);
20720
+ const video = recording?.media_shortcuts?.video_mixed;
20721
+ if (!video) return null;
20722
+ if (video.status?.code && video.status.code !== "done") return null;
20723
+ return video.data?.download_url ?? null;
20724
+ } catch (err) {
20725
+ log4(
20726
+ `could not resolve video url for recording ${recordingId}: ${err.message}`
20727
+ );
20728
+ return null;
20729
+ }
20730
+ },
20650
20731
  /**
20651
20732
  * Run the selected {prompt, agent} pairs against the transcript and store the
20652
20733
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -21542,11 +21623,11 @@ function createSDL(tables, contexts, tools, config, evals) {
21542
21623
  `;
21543
21624
  }
21544
21625
  mutationDefs += `
21545
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
21626
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21546
21627
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
21547
21628
 
21548
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
21549
- ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
21629
+ ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21630
+ ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21550
21631
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
21551
21632
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
21552
21633
  `;
@@ -30252,7 +30333,11 @@ var transcriptionsContext = new ExuluContext2({
30252
30333
  // Post-processing results carried from the transcription job at finalize:
30253
30334
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
30254
30335
  // transcripts only for now.
30255
- { name: "post_processing", type: "json", editable: false }
30336
+ { name: "post_processing", type: "json", editable: false },
30337
+ // Link back to the Recall recording so the mixed video stays reachable
30338
+ // (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
30339
+ // after six hours). Null for Whisper uploads.
30340
+ { name: "recall_recording_id", type: "text" }
30256
30341
  ],
30257
30342
  sources: [],
30258
30343
  active: true,
@@ -31606,6 +31691,16 @@ init_supervisor();
31606
31691
  init_cjs_shims();
31607
31692
  init_client();
31608
31693
  init_sanitize_name();
31694
+
31695
+ // src/exulu/context-fields-for-sync.ts
31696
+ init_cjs_shims();
31697
+ var contextFieldsForSync = (context) => (context.fields ?? []).filter((field) => !!field?.name && !!field?.type).map((field) => ({
31698
+ ...field,
31699
+ name: field.type === "file" ? `${field.name}_s3key` : field.name
31700
+ }));
31701
+
31702
+ // src/postgres/init-exulu-db.ts
31703
+ init_table_names();
31609
31704
  var {
31610
31705
  agentsSchema: agentsSchema3,
31611
31706
  feedbackSchema: feedbackSchema3,
@@ -31652,8 +31747,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
31652
31747
  await knex.schema.alterTable(tableName, (table) => {
31653
31748
  mapType(table, type, sanitizedName, defaultValue, unique);
31654
31749
  });
31750
+ } else {
31751
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31655
31752
  }
31656
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31657
31753
  }
31658
31754
  };
31659
31755
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -31837,11 +31933,18 @@ var up = async function(knex) {
31837
31933
  }
31838
31934
  };
31839
31935
  var contextDatabases = async (contexts) => {
31936
+ const { db: knex } = await postgresClient();
31840
31937
  for (const context of contexts) {
31841
31938
  const itemsTableExists = await context.tableExists();
31842
31939
  if (!itemsTableExists) {
31843
31940
  console.log("[EXULU] items table does not exist, creating it.");
31844
31941
  await context.createItemsTable();
31942
+ } else {
31943
+ await addMissingFields(
31944
+ knex,
31945
+ getTableName(context.id),
31946
+ contextFieldsForSync(context)
31947
+ );
31845
31948
  }
31846
31949
  const chunksTableExists = await context.chunksTableExists();
31847
31950
  if (!chunksTableExists && context.embedder) {
@@ -33722,8 +33825,58 @@ async function rerank(input) {
33722
33825
 
33723
33826
  // src/index.ts
33724
33827
  init_pipeline();
33828
+
33829
+ // src/exulu/models/public.ts
33830
+ init_cjs_shims();
33831
+ init_resolve_model();
33832
+ init_client();
33833
+ init_micro_call();
33834
+ var ExuluModels = {
33835
+ /**
33836
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
33837
+ *
33838
+ * Pass `userId` wherever one is known — it is what carries the caller's
33839
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
33840
+ * that user's team budget. Resolution still succeeds without it; the calls
33841
+ * are simply unattributed.
33842
+ */
33843
+ resolve: async ({
33844
+ modelId,
33845
+ userId
33846
+ }) => {
33847
+ let user;
33848
+ if (userId != null) {
33849
+ const { db: db2 } = await postgresClient();
33850
+ user = await db2("users").where({ id: userId }).first();
33851
+ }
33852
+ const { languageModel } = await resolveModel({ modelId, user: user || void 0 });
33853
+ return languageModel;
33854
+ },
33855
+ /**
33856
+ * Provider options for a constrained structured-output call.
33857
+ *
33858
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
33859
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
33860
+ * undefined for non-Gemini models.
33861
+ */
33862
+ providerOptions: (model) => microCallProviderOptions(model)
33863
+ };
33864
+
33865
+ // src/index.ts
33725
33866
  init_statistics();
33726
33867
  init_errors();
33868
+
33869
+ // src/exulu/recall/public.ts
33870
+ init_cjs_shims();
33871
+ var ExuluRecall = {
33872
+ /**
33873
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
33874
+ * of use — the URL expires after six hours.
33875
+ */
33876
+ getRecordingVideoUrl: (recordingId) => recallService.getRecordingVideoUrl(recordingId)
33877
+ };
33878
+
33879
+ // src/index.ts
33727
33880
  var ExuluJobs = {
33728
33881
  redis: redisClient
33729
33882
  };
@@ -33821,10 +33974,12 @@ var ExuluPython = {
33821
33974
  ExuluDocumentProcessor,
33822
33975
  ExuluEval,
33823
33976
  ExuluJobs,
33977
+ ExuluModels,
33824
33978
  ExuluOtel,
33825
33979
  ExuluPython,
33826
33980
  ExuluQueues,
33827
33981
  ExuluReadApi,
33982
+ ExuluRecall,
33828
33983
  ExuluReranker,
33829
33984
  ExuluTool,
33830
33985
  ExuluVariables,
package/dist/index.d.cts CHANGED
@@ -2506,6 +2506,37 @@ declare function createAgenticRetrievalTool(opts: {
2506
2506
  projectScope?: ProjectScope;
2507
2507
  }): ExuluTool | undefined;
2508
2508
 
2509
+ /**
2510
+ * Model access for consuming projects.
2511
+ *
2512
+ * Exists because ExuluApp exposes no model route and resolveModel is internal,
2513
+ * so a consumer's only alternative was building its own provider against
2514
+ * PROXY_BASE_URL — which produces untagged calls that per-team LiteLLM budgets
2515
+ * cannot attribute.
2516
+ */
2517
+ declare const ExuluModels: {
2518
+ /**
2519
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
2520
+ *
2521
+ * Pass `userId` wherever one is known — it is what carries the caller's
2522
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
2523
+ * that user's team budget. Resolution still succeeds without it; the calls
2524
+ * are simply unattributed.
2525
+ */
2526
+ resolve: ({ modelId, userId, }: {
2527
+ modelId: string;
2528
+ userId?: number;
2529
+ }) => Promise<LanguageModel>;
2530
+ /**
2531
+ * Provider options for a constrained structured-output call.
2532
+ *
2533
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
2534
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
2535
+ * undefined for non-Gemini models.
2536
+ */
2537
+ providerOptions: (model: LanguageModel | string) => Record<string, Record<string, string>> | undefined;
2538
+ };
2539
+
2509
2540
  type JOB_STATUS = "completed" | "failed" | "delayed" | "active" | "waiting" | "paused" | "stuck" | "waiting_approval" | "filtered" | "cancelled";
2510
2541
  declare const JOB_STATUS_ENUM: {
2511
2542
  completed: string;
@@ -2526,9 +2557,19 @@ declare class CredentialInvalidError extends Error {
2526
2557
  constructor(provider: string, reason?: string);
2527
2558
  }
2528
2559
 
2560
+ /** Public Recall surface for consuming projects. */
2561
+ declare const ExuluRecall: {
2562
+ /**
2563
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
2564
+ * of use — the URL expires after six hours.
2565
+ */
2566
+ getRecordingVideoUrl: (recordingId: string) => Promise<string | null>;
2567
+ };
2568
+
2529
2569
  declare const ExuluJobs: {
2530
2570
  redis: typeof redisClient;
2531
2571
  };
2572
+
2532
2573
  declare const ExuluDefaultTools: {
2533
2574
  agentic: {
2534
2575
  retrieval: {
@@ -2600,4 +2641,4 @@ declare const ExuluPython: {
2600
2641
  instructions: typeof getPythonSetupInstructions;
2601
2642
  };
2602
2643
 
2603
- export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
2644
+ export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, ExuluModels, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluRecall, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
package/dist/index.d.ts CHANGED
@@ -2506,6 +2506,37 @@ declare function createAgenticRetrievalTool(opts: {
2506
2506
  projectScope?: ProjectScope;
2507
2507
  }): ExuluTool | undefined;
2508
2508
 
2509
+ /**
2510
+ * Model access for consuming projects.
2511
+ *
2512
+ * Exists because ExuluApp exposes no model route and resolveModel is internal,
2513
+ * so a consumer's only alternative was building its own provider against
2514
+ * PROXY_BASE_URL — which produces untagged calls that per-team LiteLLM budgets
2515
+ * cannot attribute.
2516
+ */
2517
+ declare const ExuluModels: {
2518
+ /**
2519
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
2520
+ *
2521
+ * Pass `userId` wherever one is known — it is what carries the caller's
2522
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
2523
+ * that user's team budget. Resolution still succeeds without it; the calls
2524
+ * are simply unattributed.
2525
+ */
2526
+ resolve: ({ modelId, userId, }: {
2527
+ modelId: string;
2528
+ userId?: number;
2529
+ }) => Promise<LanguageModel>;
2530
+ /**
2531
+ * Provider options for a constrained structured-output call.
2532
+ *
2533
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
2534
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
2535
+ * undefined for non-Gemini models.
2536
+ */
2537
+ providerOptions: (model: LanguageModel | string) => Record<string, Record<string, string>> | undefined;
2538
+ };
2539
+
2509
2540
  type JOB_STATUS = "completed" | "failed" | "delayed" | "active" | "waiting" | "paused" | "stuck" | "waiting_approval" | "filtered" | "cancelled";
2510
2541
  declare const JOB_STATUS_ENUM: {
2511
2542
  completed: string;
@@ -2526,9 +2557,19 @@ declare class CredentialInvalidError extends Error {
2526
2557
  constructor(provider: string, reason?: string);
2527
2558
  }
2528
2559
 
2560
+ /** Public Recall surface for consuming projects. */
2561
+ declare const ExuluRecall: {
2562
+ /**
2563
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
2564
+ * of use — the URL expires after six hours.
2565
+ */
2566
+ getRecordingVideoUrl: (recordingId: string) => Promise<string | null>;
2567
+ };
2568
+
2529
2569
  declare const ExuluJobs: {
2530
2570
  redis: typeof redisClient;
2531
2571
  };
2572
+
2532
2573
  declare const ExuluDefaultTools: {
2533
2574
  agentic: {
2534
2575
  retrieval: {
@@ -2600,4 +2641,4 @@ declare const ExuluPython: {
2600
2641
  instructions: typeof getPythonSetupInstructions;
2601
2642
  };
2602
2643
 
2603
- export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
2644
+ export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, ExuluModels, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluRecall, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  listS3ObjectsByPrefix,
67
67
  listTagsByPrefix,
68
68
  mapStreamErrorMessage,
69
+ microCallProviderOptions,
69
70
  parseResetAt,
70
71
  postgresClient,
71
72
  provisionDefaultUserBudget,
@@ -87,7 +88,7 @@ import {
87
88
  verifyCredentialNonce,
88
89
  waitForLiteLLMReady,
89
90
  withRetry
90
- } from "./chunk-7AZH4ETH.js";
91
+ } from "./chunk-BNTL6LYY.js";
91
92
  import {
92
93
  LiteLLMAdminError,
93
94
  findLiteLLMModel,
@@ -4804,7 +4805,7 @@ var ExuluContext2 = class {
4804
4805
  }
4805
4806
  console.log("[EXULU] context configuration", this.configuration);
4806
4807
  let jobs = [];
4807
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
4808
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
4808
4809
  if (this.processor) {
4809
4810
  const processor = this.processor;
4810
4811
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4826,12 +4827,12 @@ var ExuluContext2 = class {
4826
4827
  ...processorResult
4827
4828
  });
4828
4829
  if (processor.config?.generateEmbeddings) {
4829
- shouldGenerateEmbeddings = true;
4830
+ shouldGenerateEmbeddings2 = true;
4830
4831
  }
4831
4832
  }
4832
4833
  }
4833
4834
  }
4834
- if (shouldGenerateEmbeddings) {
4835
+ if (shouldGenerateEmbeddings2) {
4835
4836
  console.log("[EXULU] generating embeddings for item", results[0].id);
4836
4837
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4837
4838
  item: {
@@ -4875,7 +4876,7 @@ var ExuluContext2 = class {
4875
4876
  }).returning("id");
4876
4877
  await mutation;
4877
4878
  let jobs = [];
4878
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
4879
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
4879
4880
  if (this.processor) {
4880
4881
  const processor = this.processor;
4881
4882
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4897,12 +4898,12 @@ var ExuluContext2 = class {
4897
4898
  ...processorResult
4898
4899
  });
4899
4900
  if (processor.config?.generateEmbeddings) {
4900
- shouldGenerateEmbeddings = true;
4901
+ shouldGenerateEmbeddings2 = true;
4901
4902
  }
4902
4903
  }
4903
4904
  }
4904
4905
  }
4905
- if (shouldGenerateEmbeddings) {
4906
+ if (shouldGenerateEmbeddings2) {
4906
4907
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4907
4908
  item: record,
4908
4909
  // important we need to full record here with all fields for the embedder
@@ -6707,6 +6708,17 @@ var applyAgentGuestFieldTransforms = async (input) => {
6707
6708
  return input;
6708
6709
  };
6709
6710
 
6711
+ // src/graphql/mutations/should-generate-embeddings.ts
6712
+ var shouldGenerateEmbeddings = ({
6713
+ calculateVectors,
6714
+ operation,
6715
+ override
6716
+ }) => {
6717
+ if (override !== void 0) return override;
6718
+ if (calculateVectors === "always") return true;
6719
+ return operation === "create" ? calculateVectors === "onInsert" : calculateVectors === "onUpdate";
6720
+ };
6721
+
6710
6722
  // src/graphql/mutations/index.ts
6711
6723
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
6712
6724
  var postprocessDeletion = async ({
@@ -6775,7 +6787,9 @@ var postprocessUpdate = async ({
6775
6787
  result,
6776
6788
  user,
6777
6789
  role,
6778
- config
6790
+ config,
6791
+ operation,
6792
+ generateEmbeddings
6779
6793
  }) => {
6780
6794
  if (!result) {
6781
6795
  return result;
@@ -6802,7 +6816,11 @@ var postprocessUpdate = async ({
6802
6816
  if (!context.embedder) {
6803
6817
  return result;
6804
6818
  }
6805
- if (context.embedder && (context.configuration.calculateVectors === "onUpdate" || context.configuration.calculateVectors === "always")) {
6819
+ if (shouldGenerateEmbeddings({
6820
+ calculateVectors: context.configuration.calculateVectors,
6821
+ operation,
6822
+ override: generateEmbeddings
6823
+ })) {
6806
6824
  const { db } = await postgresClient();
6807
6825
  console.log("[EXULU] Deleting chunks for item", result.id);
6808
6826
  const exists = await context.chunksTableExists();
@@ -7052,7 +7070,9 @@ function createMutations(table, contexts, tools, config) {
7052
7070
  result: results[0],
7053
7071
  user: context.user.id,
7054
7072
  role: context.user.role?.id,
7055
- config
7073
+ config,
7074
+ operation: "create",
7075
+ generateEmbeddings: args.generateEmbeddings
7056
7076
  });
7057
7077
  return {
7058
7078
  // Filter result to only include requested fields
@@ -7126,7 +7146,9 @@ function createMutations(table, contexts, tools, config) {
7126
7146
  result,
7127
7147
  user: context.user.id,
7128
7148
  role: context.user.role?.id,
7129
- config
7149
+ config,
7150
+ operation: "update",
7151
+ generateEmbeddings: args.generateEmbeddings
7130
7152
  });
7131
7153
  return {
7132
7154
  item: finalizeRequestedFields({
@@ -7190,7 +7212,9 @@ function createMutations(table, contexts, tools, config) {
7190
7212
  result,
7191
7213
  user: context.user.id,
7192
7214
  role: context.user.role?.id,
7193
- config
7215
+ config,
7216
+ operation: "update",
7217
+ generateEmbeddings: args.generateEmbeddings
7194
7218
  });
7195
7219
  return {
7196
7220
  item: finalizeRequestedFields({
@@ -7460,6 +7484,7 @@ function createMutations(table, contexts, tools, config) {
7460
7484
  };
7461
7485
  }
7462
7486
  query = applyFilters(query, args.where, table);
7487
+ query = applyAccessControl(table, query, context.user);
7463
7488
  if (args.limit) {
7464
7489
  query = query.limit(args.limit);
7465
7490
  }
@@ -10835,6 +10860,32 @@ var renderTranscript = (segments, speakers) => {
10835
10860
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
10836
10861
  };
10837
10862
 
10863
+ // src/exulu/transcription/build-transcript-item.ts
10864
+ var buildTranscriptItemInput = ({
10865
+ row,
10866
+ title,
10867
+ speakers,
10868
+ transcriptText,
10869
+ rightsMode,
10870
+ isReSave
10871
+ }) => ({
10872
+ // Carrying the id on re-save makes context.createItem upsert in place.
10873
+ ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
10874
+ name: title ?? row.title ?? "Transcript",
10875
+ transcript_text: transcriptText,
10876
+ audio_s3key: row.audio_s3key,
10877
+ language: row.language ?? void 0,
10878
+ duration_seconds: row.duration_seconds ?? void 0,
10879
+ speakers,
10880
+ raw_segments: row.raw_segments,
10881
+ // Recall meeting-bot post-processing results (null for Whisper jobs).
10882
+ post_processing: row.post_processing_outputs ?? void 0,
10883
+ // Handle for the meeting video; null for Whisper uploads.
10884
+ recall_recording_id: row.recall_recording_id ?? void 0,
10885
+ rights_mode: rightsMode,
10886
+ created_by: row.created_by
10887
+ });
10888
+
10838
10889
  // src/exulu/transcription/service.ts
10839
10890
  var TABLE = "transcription_jobs";
10840
10891
  var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -11027,21 +11078,14 @@ var transcriptionService = {
11027
11078
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
11028
11079
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
11029
11080
  const isReSave = row.status === "saved" && !!row.saved_item_id;
11030
- const itemInput = {
11031
- // Carrying the id on re-save makes context.createItem upsert in place.
11032
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
11033
- name: input.title ?? row.title ?? "Transcript",
11034
- transcript_text: transcriptText,
11035
- audio_s3key: row.audio_s3key,
11036
- language: row.language ?? void 0,
11037
- duration_seconds: row.duration_seconds ?? void 0,
11081
+ const itemInput = buildTranscriptItemInput({
11082
+ row,
11083
+ title: input.title,
11038
11084
  speakers: input.speakers,
11039
- raw_segments: row.raw_segments,
11040
- // Recall meeting-bot post-processing results (null for Whisper jobs).
11041
- post_processing: row.post_processing_outputs ?? void 0,
11042
- rights_mode: rightsMode,
11043
- created_by: row.created_by
11044
- };
11085
+ transcriptText,
11086
+ rightsMode,
11087
+ isReSave
11088
+ });
11045
11089
  let item;
11046
11090
  try {
11047
11091
  const result = await context.createItem(
@@ -11279,25 +11323,34 @@ var recordingDurationSeconds = (rec) => {
11279
11323
  }
11280
11324
  return null;
11281
11325
  };
11326
+ var RECORDING_RETENTION_HOURS = 2160;
11327
+ var buildCreateBotPayload = (input) => ({
11328
+ meeting_url: input.meeting_url,
11329
+ join_at: input.join_at,
11330
+ ...input.bot_name ? { bot_name: input.bot_name } : {},
11331
+ recording_config: {
11332
+ video_mixed_mp4: {},
11333
+ video_mixed_layout: "speaker_view",
11334
+ participant_events: {},
11335
+ meeting_metadata: {},
11336
+ retention: { type: "timed", hours: RECORDING_RETENTION_HOURS }
11337
+ },
11338
+ ...input.notifyChat ? {
11339
+ chat: {
11340
+ on_bot_join: {
11341
+ send_to: "everyone",
11342
+ message: input.notifyChat.message,
11343
+ pin: true
11344
+ }
11345
+ }
11346
+ } : {}
11347
+ });
11282
11348
  var recallClient = {
11283
11349
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
11284
11350
  /** POST /bot — schedule/launch a bot for a meeting. */
11285
11351
  createBot: (input) => request2("/bot/", {
11286
11352
  method: "POST",
11287
- body: JSON.stringify({
11288
- meeting_url: input.meeting_url,
11289
- join_at: input.join_at,
11290
- ...input.bot_name ? { bot_name: input.bot_name } : {},
11291
- ...input.notifyChat ? {
11292
- chat: {
11293
- on_bot_join: {
11294
- send_to: "everyone",
11295
- message: input.notifyChat.message,
11296
- pin: true
11297
- }
11298
- }
11299
- } : {}
11300
- })
11353
+ body: JSON.stringify(buildCreateBotPayload(input))
11301
11354
  }),
11302
11355
  /**
11303
11356
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -11612,6 +11665,28 @@ var recallService = {
11612
11665
  }
11613
11666
  return null;
11614
11667
  },
11668
+ /**
11669
+ * Fresh signed URL for a recording's mixed MP4, or null when there is no
11670
+ * usable video (still processing, no video artifact, recording expired or
11671
+ * deleted).
11672
+ *
11673
+ * Never cache the result: the URL carries X-Amz-Expires=21600, so it dies
11674
+ * after six hours. Resolve at point of use.
11675
+ */
11676
+ async getRecordingVideoUrl(recordingId) {
11677
+ try {
11678
+ const recording = await recallClient.retrieveRecording(recordingId);
11679
+ const video = recording?.media_shortcuts?.video_mixed;
11680
+ if (!video) return null;
11681
+ if (video.status?.code && video.status.code !== "done") return null;
11682
+ return video.data?.download_url ?? null;
11683
+ } catch (err) {
11684
+ log3(
11685
+ `could not resolve video url for recording ${recordingId}: ${err.message}`
11686
+ );
11687
+ return null;
11688
+ }
11689
+ },
11615
11690
  /**
11616
11691
  * Run the selected {prompt, agent} pairs against the transcript and store the
11617
11692
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -12499,11 +12574,11 @@ function createSDL(tables, contexts, tools, config, evals) {
12499
12574
  `;
12500
12575
  }
12501
12576
  mutationDefs += `
12502
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
12577
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12503
12578
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
12504
12579
 
12505
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
12506
- ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
12580
+ ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12581
+ ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12507
12582
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
12508
12583
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
12509
12584
  `;
@@ -20890,7 +20965,11 @@ var transcriptionsContext = new ExuluContext2({
20890
20965
  // Post-processing results carried from the transcription job at finalize:
20891
20966
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
20892
20967
  // transcripts only for now.
20893
- { name: "post_processing", type: "json", editable: false }
20968
+ { name: "post_processing", type: "json", editable: false },
20969
+ // Link back to the Recall recording so the mixed video stays reachable
20970
+ // (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
20971
+ // after six hours). Null for Whisper uploads.
20972
+ { name: "recall_recording_id", type: "text" }
20894
20973
  ],
20895
20974
  sources: [],
20896
20975
  active: true,
@@ -22219,6 +22298,12 @@ var ExuluReadApi = {
22219
22298
  embedQuery
22220
22299
  };
22221
22300
 
22301
+ // src/exulu/context-fields-for-sync.ts
22302
+ var contextFieldsForSync = (context) => (context.fields ?? []).filter((field) => !!field?.name && !!field?.type).map((field) => ({
22303
+ ...field,
22304
+ name: field.type === "file" ? `${field.name}_s3key` : field.name
22305
+ }));
22306
+
22222
22307
  // src/postgres/init-exulu-db.ts
22223
22308
  var {
22224
22309
  agentsSchema: agentsSchema3,
@@ -22266,8 +22351,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
22266
22351
  await knex.schema.alterTable(tableName, (table) => {
22267
22352
  mapType(table, type, sanitizedName, defaultValue, unique);
22268
22353
  });
22354
+ } else {
22355
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22269
22356
  }
22270
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22271
22357
  }
22272
22358
  };
22273
22359
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -22451,11 +22537,18 @@ var up = async function(knex) {
22451
22537
  }
22452
22538
  };
22453
22539
  var contextDatabases = async (contexts) => {
22540
+ const { db: knex } = await postgresClient();
22454
22541
  for (const context of contexts) {
22455
22542
  const itemsTableExists = await context.tableExists();
22456
22543
  if (!itemsTableExists) {
22457
22544
  console.log("[EXULU] items table does not exist, creating it.");
22458
22545
  await context.createItemsTable();
22546
+ } else {
22547
+ await addMissingFields(
22548
+ knex,
22549
+ getTableName(context.id),
22550
+ contextFieldsForSync(context)
22551
+ );
22459
22552
  }
22460
22553
  const chunksTableExists = await context.chunksTableExists();
22461
22554
  if (!chunksTableExists && context.embedder) {
@@ -24312,6 +24405,47 @@ async function rerank(input) {
24312
24405
  return resolved.rerank(query, items, { topN });
24313
24406
  }
24314
24407
 
24408
+ // src/exulu/models/public.ts
24409
+ var ExuluModels = {
24410
+ /**
24411
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
24412
+ *
24413
+ * Pass `userId` wherever one is known — it is what carries the caller's
24414
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
24415
+ * that user's team budget. Resolution still succeeds without it; the calls
24416
+ * are simply unattributed.
24417
+ */
24418
+ resolve: async ({
24419
+ modelId,
24420
+ userId
24421
+ }) => {
24422
+ let user;
24423
+ if (userId != null) {
24424
+ const { db } = await postgresClient();
24425
+ user = await db("users").where({ id: userId }).first();
24426
+ }
24427
+ const { languageModel } = await resolveModel({ modelId, user: user || void 0 });
24428
+ return languageModel;
24429
+ },
24430
+ /**
24431
+ * Provider options for a constrained structured-output call.
24432
+ *
24433
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
24434
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
24435
+ * undefined for non-Gemini models.
24436
+ */
24437
+ providerOptions: (model) => microCallProviderOptions(model)
24438
+ };
24439
+
24440
+ // src/exulu/recall/public.ts
24441
+ var ExuluRecall = {
24442
+ /**
24443
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
24444
+ * of use — the URL expires after six hours.
24445
+ */
24446
+ getRecordingVideoUrl: (recordingId) => recallService.getRecordingVideoUrl(recordingId)
24447
+ };
24448
+
24315
24449
  // src/index.ts
24316
24450
  var ExuluJobs = {
24317
24451
  redis: redisClient
@@ -24409,10 +24543,12 @@ export {
24409
24543
  ExuluDocumentProcessor,
24410
24544
  ExuluEval,
24411
24545
  ExuluJobs,
24546
+ ExuluModels,
24412
24547
  ExuluOtel,
24413
24548
  ExuluPython,
24414
24549
  queues as ExuluQueues,
24415
24550
  ExuluReadApi,
24551
+ ExuluRecall,
24416
24552
  ExuluReranker,
24417
24553
  ExuluTool,
24418
24554
  ExuluVariables,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.6.0",
4
+ "version": "3.7.1",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {