@memberjunction/server 5.36.0 → 5.38.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.
Files changed (38) hide show
  1. package/README.md +12 -12
  2. package/dist/agents/skip-agent.d.ts.map +1 -1
  3. package/dist/agents/skip-agent.js +14 -2
  4. package/dist/agents/skip-agent.js.map +1 -1
  5. package/dist/agents/skip-sdk.d.ts +10 -0
  6. package/dist/agents/skip-sdk.d.ts.map +1 -1
  7. package/dist/agents/skip-sdk.js +155 -13
  8. package/dist/agents/skip-sdk.js.map +1 -1
  9. package/dist/generated/generated.d.ts +97 -5
  10. package/dist/generated/generated.d.ts.map +1 -1
  11. package/dist/generated/generated.js +553 -13
  12. package/dist/generated/generated.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +57 -14
  15. package/dist/index.js.map +1 -1
  16. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  17. package/dist/resolvers/AdhocQueryResolver.js +42 -28
  18. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  19. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  20. package/dist/resolvers/QueryResolver.js +5 -4
  21. package/dist/resolvers/QueryResolver.js.map +1 -1
  22. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  23. package/dist/resolvers/QuerySystemUserResolver.js +6 -18
  24. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  25. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  26. package/dist/resolvers/RunAIAgentResolver.js +6 -59
  27. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  28. package/package.json +70 -70
  29. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +269 -0
  30. package/src/__tests__/resolverBase.rls.test.ts +224 -0
  31. package/src/agents/skip-agent.ts +15 -2
  32. package/src/agents/skip-sdk.ts +163 -13
  33. package/src/generated/generated.ts +388 -14
  34. package/src/index.ts +62 -17
  35. package/src/resolvers/AdhocQueryResolver.ts +42 -28
  36. package/src/resolvers/QueryResolver.ts +7 -6
  37. package/src/resolvers/QuerySystemUserResolver.ts +11 -25
  38. package/src/resolvers/RunAIAgentResolver.ts +6 -66
@@ -358,13 +358,25 @@ export class SkipProxyAgent extends BaseAgent {
358
358
  const skipMessage = response.messages?.filter(msg => msg.role === 'system').pop();
359
359
  const analysisText = response.analysis?.trim();
360
360
 
361
+ // Skip-Brain attaches actionableCommands as an extension field on the
362
+ // analysis_complete response (e.g. `client:capture-data-snapshot` when
363
+ // the Analysis Agent needs the user's current view). Forward to the
364
+ // next step so AgentRunner persists them to ConversationDetail.ActionableCommands
365
+ // — which the chat UI's actionable-commands.component reads to render buttons.
366
+ const responseExt = response as unknown as { actionableCommands?: unknown[] };
367
+ const actionableCommands = Array.isArray(responseExt.actionableCommands) && responseExt.actionableCommands.length > 0
368
+ ? (responseExt.actionableCommands as BaseAgentNextStep<ComponentSpec>['actionableCommands'])
369
+ : undefined;
370
+ LogStatus(`[SkipProxyAgent DEBUG] handleAnalysisComplete actionableCommands from response: ${JSON.stringify(responseExt.actionableCommands)}`);
371
+
361
372
  if (!hasComponentOptions) {
362
373
  if (analysisText || skipMessage?.content) {
363
374
  return {
364
375
  terminate: true,
365
376
  step: 'Success',
366
377
  message: analysisText || skipMessage!.content,
367
- newPayload: undefined
378
+ newPayload: undefined,
379
+ actionableCommands
368
380
  };
369
381
  }
370
382
 
@@ -387,7 +399,8 @@ export class SkipProxyAgent extends BaseAgent {
387
399
  terminate: true,
388
400
  step: 'Success',
389
401
  message: skipMessage?.content || response.title || 'Analysis complete',
390
- newPayload: componentSpec
402
+ newPayload: componentSpec,
403
+ actionableCommands
391
404
  };
392
405
  }
393
406
 
@@ -24,7 +24,8 @@ import {
24
24
  SkipAPIArtifactType
25
25
  } from '@memberjunction/skip-types';
26
26
  import { DataContext } from '@memberjunction/data-context';
27
- import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
27
+ import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, RunView, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
28
+ import { MJConversationDetailEntity, QueryEngine } from '@memberjunction/core-entities';
28
29
  import { request as httpRequest } from 'http';
29
30
  import { request as httpsRequest } from 'https';
30
31
  import { gzip as gzipCompress, createGunzip } from 'zlib';
@@ -482,15 +483,15 @@ export class SkipSDK {
482
483
  * Build saved queries for Skip
483
484
  */
484
485
  private buildQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
485
- const md = this.Provider;
486
- const approvedQueries = md.Queries.filter((q) => q.Status === status);
486
+ const qe = QueryEngine.Instance;
487
+ const approvedQueries = qe.Queries.filter((q) => q.Status === status);
487
488
 
488
489
  return approvedQueries.map((q) => ({
489
490
  ID: q.ID,
490
491
  Name: q.Name,
491
492
  Description: q.Description,
492
493
  Category: q.Category,
493
- CategoryPath: this.buildQueryCategoryPath(md, q.CategoryID),
494
+ CategoryPath: this.buildQueryCategoryPath(qe, q.CategoryID),
494
495
  CategoryID: q.CategoryID,
495
496
  SQL: q.SQL,
496
497
  Status: q.Status,
@@ -500,7 +501,7 @@ export class SkipSDK {
500
501
  EmbeddingModelID: q.EmbeddingModelID,
501
502
  EmbeddingModelName: q.EmbeddingModel,
502
503
  TechnicalDescription: q.TechnicalDescription,
503
- Fields: q.Fields.map((f) => ({
504
+ Fields: qe.GetQueryFields(q.ID).map((f) => ({
504
505
  ID: f.ID,
505
506
  QueryID: f.QueryID,
506
507
  Name: f.Name,
@@ -516,7 +517,7 @@ export class SkipSDK {
516
517
  IsSummary: f.IsSummary,
517
518
  SummaryDescription: f.SummaryDescription
518
519
  })),
519
- Parameters: q.Parameters.map((p) => ({
520
+ Parameters: qe.GetQueryParameters(q.ID).map((p) => ({
520
521
  ID: p.ID,
521
522
  QueryID: p.QueryID,
522
523
  Name: p.Name,
@@ -527,12 +528,15 @@ export class SkipSDK {
527
528
  SampleValue: p.SampleValue,
528
529
  ValidationFilters: p.ValidationFilters
529
530
  })),
530
- Entities: q.Entities.map((e) => ({
531
+ Entities: qe.QueryEntities.filter(e => UUIDsEqual(e.QueryID, q.ID)).map((e) => ({
531
532
  ID: e.ID,
532
533
  QueryID: e.QueryID,
533
534
  EntityID: e.EntityID,
534
535
  Entity: e.Entity
535
536
  })),
537
+ SQLDialectID: q.SQLDialectID,
538
+ UsesTemplate: q.UsesTemplate,
539
+ IsApproved: q.IsApproved,
536
540
  CacheEnabled: q.CacheEnabled,
537
541
  CacheMaxSize: q.CacheMaxSize,
538
542
  CacheTTLMinutes: q.CacheTTLMinutes,
@@ -543,11 +547,11 @@ export class SkipSDK {
543
547
  /**
544
548
  * Recursively build category path for a query
545
549
  */
546
- private buildQueryCategoryPath(md: IMetadataProvider, categoryID: string): string {
547
- const cat = md.QueryCategories.find((c) => UUIDsEqual(c.ID, categoryID));
550
+ private buildQueryCategoryPath(qe: QueryEngine, categoryID: string): string {
551
+ const cat = qe.Categories.find((c) => UUIDsEqual(c.ID, categoryID));
548
552
  if (!cat) return '';
549
553
  if (!cat.ParentID) return cat.Name;
550
- const parentPath = this.buildQueryCategoryPath(md, cat.ParentID);
554
+ const parentPath = this.buildQueryCategoryPath(qe, cat.ParentID);
551
555
  return parentPath ? `${parentPath}/${cat.Name}` : cat.Name;
552
556
  }
553
557
 
@@ -559,11 +563,11 @@ export class SkipSDK {
559
563
  * across all queries, not just approved ones.
560
564
  */
561
565
  private buildQueryCatalog(): SkipQueryCatalogEntry[] {
562
- const md = this.Provider;
566
+ const qe = QueryEngine.Instance;
563
567
 
564
- return md.Queries.map((q) => ({
568
+ return qe.Queries.map((q) => ({
565
569
  Name: q.Name,
566
- CategoryPath: this.buildQueryCategoryPath(md, q.CategoryID)
570
+ CategoryPath: this.buildQueryCategoryPath(qe, q.CategoryID)
567
571
  }));
568
572
  }
569
573
 
@@ -682,6 +686,16 @@ export class SkipSDK {
682
686
  versions: entry.versions
683
687
  }));
684
688
 
689
+ // Also include INPUT artifacts (e.g., user-captured Data Snapshots
690
+ // attached to messages via the Analyze button or
691
+ // client:capture-data-snapshot actionable command). The query above
692
+ // only returns Direction='Output' artifacts produced BY Skip — but
693
+ // Skip also needs to see artifacts the user gave it as input.
694
+ const inputArtifacts = await this.buildInputArtifacts(contextUser, conversationId, artifactMap);
695
+ if (inputArtifacts.length > 0) {
696
+ artifacts.push(...inputArtifacts);
697
+ }
698
+
685
699
  return artifacts;
686
700
  } catch (error) {
687
701
  LogError(`Failed to build artifacts for conversation ${conversationId}: ${error}`);
@@ -689,6 +703,142 @@ export class SkipSDK {
689
703
  }
690
704
  }
691
705
 
706
+ /**
707
+ * Fetch INPUT artifacts attached to user messages in this conversation
708
+ * (Direction='Input' on ConversationDetailArtifact). Skip should see these
709
+ * so it can use captured Data Snapshots, user-uploaded files, etc., in
710
+ * its analysis.
711
+ *
712
+ * Deduplicates against `alreadyLoaded` (the Output artifacts) so the same
713
+ * artifact ID doesn't appear twice if it was both produced and re-attached.
714
+ */
715
+ private async buildInputArtifacts(
716
+ contextUser: UserInfo,
717
+ conversationId: string,
718
+ alreadyLoaded: Map<string, { artifact: any; artifactType: SkipAPIArtifactType; versions: SkipAPIArtifactVersion[] }>
719
+ ): Promise<SkipAPIArtifact[]> {
720
+ try {
721
+ const rv = new RunView();
722
+ // Pull conversation detail IDs in this conversation
723
+ const detailsResult = await rv.RunView<MJConversationDetailEntity>({
724
+ EntityName: 'MJ: Conversation Details',
725
+ ExtraFilter: `ConversationID='${conversationId}'`,
726
+ Fields: ['ID'],
727
+ ResultType: 'simple',
728
+ }, contextUser);
729
+ const detailIds = detailsResult.Success && detailsResult.Results
730
+ ? (detailsResult.Results as { ID: string }[]).map(r => r.ID)
731
+ : [];
732
+ if (detailIds.length === 0) return [];
733
+
734
+ // Junction rows where Direction='Input' for those details
735
+ const junctionResult = await rv.RunView({
736
+ EntityName: 'MJ: Conversation Detail Artifacts',
737
+ ExtraFilter: `ConversationDetailID IN ('${detailIds.join("','")}') AND Direction='Input'`,
738
+ Fields: ['ArtifactVersionID', 'ConversationDetailID'],
739
+ ResultType: 'simple',
740
+ }, contextUser);
741
+ const junctions = junctionResult.Success && junctionResult.Results
742
+ ? (junctionResult.Results as { ArtifactVersionID: string; ConversationDetailID: string }[])
743
+ : [];
744
+ if (junctions.length === 0) return [];
745
+
746
+ // Load each ArtifactVersion + its parent Artifact + ArtifactType
747
+ const versionIds = [...new Set(junctions.map(j => j.ArtifactVersionID))];
748
+ const versionsResult = await rv.RunView({
749
+ EntityName: 'MJ: Artifact Versions',
750
+ ExtraFilter: `ID IN ('${versionIds.join("','")}')`,
751
+ ResultType: 'simple',
752
+ }, contextUser);
753
+ const versions = versionsResult.Success && versionsResult.Results
754
+ ? (versionsResult.Results as Record<string, any>[])
755
+ : [];
756
+ if (versions.length === 0) return [];
757
+
758
+ const artifactIds = [...new Set(versions.map(v => v.ArtifactID as string))];
759
+ const artifactsResult = await rv.RunView({
760
+ EntityName: 'MJ: Artifacts',
761
+ ExtraFilter: `ID IN ('${artifactIds.join("','")}')`,
762
+ ResultType: 'simple',
763
+ }, contextUser);
764
+ const artifactRows = artifactsResult.Success && artifactsResult.Results
765
+ ? (artifactsResult.Results as Record<string, any>[])
766
+ : [];
767
+
768
+ const typeIds = [...new Set(artifactRows.map(a => a.TypeID as string))];
769
+ const typesResult = await rv.RunView({
770
+ EntityName: 'MJ: Artifact Types',
771
+ ExtraFilter: `ID IN ('${typeIds.join("','")}')`,
772
+ ResultType: 'simple',
773
+ }, contextUser);
774
+ const typeRows = typesResult.Success && typesResult.Results
775
+ ? (typesResult.Results as Record<string, any>[])
776
+ : [];
777
+ const typeMap = new Map<string, Record<string, any>>(typeRows.map(t => [t.ID as string, t]));
778
+
779
+ // Build a junction lookup: artifactVersionId -> conversationDetailId
780
+ const versionToDetail = new Map(junctions.map(j => [j.ArtifactVersionID, j.ConversationDetailID]));
781
+
782
+ // Build SkipAPIArtifact entries
783
+ const inputArtifactMap = new Map<string, { artifact: any; artifactType: SkipAPIArtifactType; versions: SkipAPIArtifactVersion[] }>();
784
+ for (const v of versions) {
785
+ const aRow = artifactRows.find(a => UUIDsEqual(a.ID, v.ArtifactID));
786
+ if (!aRow) continue;
787
+ if (alreadyLoaded.has(aRow.ID as string)) continue; // dedup
788
+
789
+ const typeRow = typeMap.get(aRow.TypeID as string);
790
+ if (!typeRow) continue;
791
+
792
+ let entry = inputArtifactMap.get(aRow.ID as string);
793
+ if (!entry) {
794
+ entry = {
795
+ artifact: {
796
+ id: aRow.ID,
797
+ conversationId,
798
+ name: aRow.Name,
799
+ description: aRow.Description || '',
800
+ sharingScope: 'None',
801
+ comments: aRow.Comments || '',
802
+ createdAt: new Date(aRow.__mj_CreatedAt),
803
+ updatedAt: new Date(aRow.__mj_UpdatedAt),
804
+ },
805
+ artifactType: {
806
+ id: typeRow.ID,
807
+ name: typeRow.Name,
808
+ description: typeRow.Description,
809
+ contentType: typeRow.ContentType,
810
+ enabled: true,
811
+ createdAt: new Date(typeRow.__mj_CreatedAt),
812
+ updatedAt: new Date(typeRow.__mj_UpdatedAt),
813
+ },
814
+ versions: [],
815
+ };
816
+ inputArtifactMap.set(aRow.ID as string, entry);
817
+ }
818
+ entry.versions.push({
819
+ id: v.ID,
820
+ artifactId: v.ArtifactID,
821
+ conversationDetailID: versionToDetail.get(v.ID) ?? '',
822
+ version: v.VersionNumber,
823
+ configuration: v.Configuration || '',
824
+ content: v.Content || '',
825
+ comments: v.Comments || '',
826
+ createdAt: new Date(v.__mj_CreatedAt),
827
+ updatedAt: new Date(v.__mj_UpdatedAt),
828
+ });
829
+ }
830
+
831
+ return Array.from(inputArtifactMap.values()).map(entry => ({
832
+ ...entry.artifact,
833
+ artifactType: entry.artifactType,
834
+ versions: entry.versions,
835
+ }));
836
+ } catch (error) {
837
+ LogError(`Failed to load input artifacts for conversation ${conversationId}: ${error}`);
838
+ return [];
839
+ }
840
+ }
841
+
692
842
  /**
693
843
  * Build API keys for AI services
694
844
  */