@newheap/platform-ai-chat 0.16.0 → 0.17.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.
@@ -25,6 +25,7 @@ const statusTable = {
25
25
  'assistant-run-filter-invalid',
26
26
  'assistant-board-scope-invalid',
27
27
  'assistant-feedback-invalid',
28
+ 'assistant-trigger-filter-not-found',
28
29
  'assistant-replay-not-shadow',
29
30
  'assistant-evaluation-not-variant',
30
31
  'assistant-instructions-too-long',
@@ -2222,6 +2223,9 @@ const maxDescription$1 = 300;
2222
2223
  const maxInstructions = 20_000;
2223
2224
  const maxMessage$1 = 2_000;
2224
2225
  const maxReason$1 = 500;
2226
+ /** A data trigger applies at most this many filters of its event, each named in lowercase dash-case. */
2227
+ const maxTriggerFilters = 10;
2228
+ const filterName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2225
2229
  /** The platform's default file limits (`NhAssistantLimits`). */
2226
2230
  const defaultFileLimits = {
2227
2231
  maxFileBytes: 20 * 1024 * 1024,
@@ -2249,7 +2253,8 @@ class NhAssistantMockRunConfiguration {
2249
2253
  this.triggers = new Map();
2250
2254
  const approvalPolicies = scenario.runCatalog?.approvalPolicies ?? scenario.policies ?? [];
2251
2255
  this.catalog = {
2252
- triggerEvents: scenario.runCatalog?.triggerEvents ?? [],
2256
+ // A trigger event lists its named filters, none unless the scenario's application registers some.
2257
+ triggerEvents: (scenario.runCatalog?.triggerEvents ?? []).map(event => ({ ...event, filters: event.filters ?? [] })),
2253
2258
  subjectSets: scenario.runCatalog?.subjectSets ?? [],
2254
2259
  ruleHandlers: scenario.runCatalog?.ruleHandlers ?? [],
2255
2260
  approvalPolicies,
@@ -2375,8 +2380,13 @@ class NhAssistantMockRunConfiguration {
2375
2380
  */
2376
2381
  triggerProblem(agent, trigger) {
2377
2382
  if (trigger.source === 'event') {
2378
- const subjectType = this.catalog.triggerEvents.find(item => item.name === trigger.event)?.subjectType;
2379
- return subjectType && !mockWorksOnCovers(agent, subjectType) ? 'not-worked-on' : null;
2383
+ const event = this.catalog.triggerEvents.find(item => item.name === trigger.event);
2384
+ if (event?.subjectType && !mockWorksOnCovers(agent, event.subjectType)) {
2385
+ return 'not-worked-on';
2386
+ }
2387
+ // A filter the event no longer offers keeps the trigger from firing.
2388
+ const offered = (event?.filters ?? []).map(filter => filter.name);
2389
+ return (trigger.filters ?? []).some(name => !offered.includes(name)) ? 'unknown-filter' : null;
2380
2390
  }
2381
2391
  return trigger.subjectSet === null && !mockWorksOnCovers(agent, mockSystemSubjectType) ? 'not-worked-on' : null;
2382
2392
  }
@@ -2706,9 +2716,20 @@ class NhAssistantMockRunConfiguration {
2706
2716
  if (skill !== null && !this.skillIds(agent).includes(skill)) {
2707
2717
  errors['skill'] = ['not-found'];
2708
2718
  }
2719
+ // The filters in the order chosen, each once; only an event trigger takes them.
2720
+ const filters = [...new Set((request.filters ?? []).map(name => (typeof name === 'string' ? name.trim() : '')))];
2721
+ if (filters.length > maxTriggerFilters || filters.some(name => name.length > 64 || !filterName.test(name)) || (schedule && filters.length > 0)) {
2722
+ errors['filters'] = ['invalid'];
2723
+ }
2709
2724
  if (Object.keys(errors).length > 0) {
2710
2725
  return mockFailure('assistant-validation', errors);
2711
2726
  }
2727
+ if (request.source === 'event') {
2728
+ const offered = (this.catalog.triggerEvents.find(item => item.name === text(request.event))?.filters ?? []).map(filter => filter.name);
2729
+ if (filters.some(name => !offered.includes(name))) {
2730
+ return mockFailure('assistant-trigger-filter-not-found', { filters: ['not-found'] });
2731
+ }
2732
+ }
2712
2733
  if ((request.expectedVersion ?? null) !== (existing?.version ?? null)) {
2713
2734
  return mockFailure('assistant-version-conflict');
2714
2735
  }
@@ -2729,7 +2750,8 @@ class NhAssistantMockRunConfiguration {
2729
2750
  changedBy: 'current-user',
2730
2751
  reason: text(request.reason),
2731
2752
  inEffect: true,
2732
- problem: null
2753
+ problem: null,
2754
+ filters: schedule ? [] : filters
2733
2755
  };
2734
2756
  this.triggers.set(triggerId, Object.assign(saved, { [ownerKey]: agent.id }));
2735
2757
  return { status: 200, body: withoutOwner(saved) };
@@ -3653,7 +3675,7 @@ class NhAssistantMockAgentRuns {
3653
3675
  result: this.answerOf(found.run.id, language),
3654
3676
  resultTruncated: this.truncated.has(found.run.id),
3655
3677
  summary: found.run.summary,
3656
- proposals: [],
3678
+ proposals: this.context.runProposals?.(found.run.id) ?? [],
3657
3679
  hiddenProposals: 0,
3658
3680
  artifacts: [],
3659
3681
  canOpenConversation: this.mayView(found.thread, language)
@@ -4388,7 +4410,7 @@ const maxGroupId = 64;
4388
4410
  const maxGroups = 100;
4389
4411
  const maxName = 100;
4390
4412
  const maxDescription = 300;
4391
- const guid = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
4413
+ const guid$1 = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
4392
4414
  /**
4393
4415
  * Groups and boards of the mock API (K15, K16), as the server serves them: the scenario's
4394
4416
  * `agentGroups` are the groups in code with their names per language, checked as the server checks
@@ -4515,7 +4537,7 @@ class NhAssistantMockBoards {
4515
4537
  }
4516
4538
  const groupId = segments[1];
4517
4539
  const divisionId = query.get('divisionId');
4518
- if (divisionId !== null && divisionId !== '' && !guid.test(divisionId)) {
4540
+ if (divisionId !== null && divisionId !== '' && !guid$1.test(divisionId)) {
4519
4541
  return mockFailure('assistant-board-scope-invalid');
4520
4542
  }
4521
4543
  const division = divisionId ? this.division(divisionId, requested) : null;
@@ -4656,6 +4678,7 @@ const dashCase = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
4656
4678
  const maxReason = 500;
4657
4679
  const maxItemsPerPage = 100;
4658
4680
  const maxBulkDecisions = 50;
4681
+ const guid = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
4659
4682
  const lifetimeMs = 3 * 24 * 60 * 60 * 1000;
4660
4683
  /** The platform's subject type of the whole application. */
4661
4684
  const systemType = 'nh-system';
@@ -4671,12 +4694,15 @@ const systemType = 'nh-system';
4671
4694
  * answers `404` and deciding it `404 assistant-approval-not-found`.
4672
4695
  */
4673
4696
  class NhAssistantMockProposals {
4674
- constructor(scenario = [], actorId, now, mayDecideApplication = () => true, decided = () => undefined) {
4697
+ constructor(scenario = [], actorId, now, mayDecideApplication = () => true, decided = () => undefined, mayWorkIn = () => true) {
4675
4698
  this.actorId = actorId;
4676
4699
  this.now = now;
4677
4700
  this.mayDecideApplication = mayDecideApplication;
4678
4701
  this.decided = decided;
4702
+ this.mayWorkIn = mayWorkIn;
4679
4703
  this.proposals = new Map();
4704
+ /** When the caller last looked at the inbox, by scope key. */
4705
+ this.visits = new Map();
4680
4706
  const created = new Date(now()).getTime();
4681
4707
  for (const item of scenario) {
4682
4708
  if (this.proposals.has(item.approvalId)) {
@@ -4707,8 +4733,8 @@ class NhAssistantMockProposals {
4707
4733
  mode: 'deferred',
4708
4734
  policyId: null,
4709
4735
  grantSource: 'agent',
4710
- createdAt: new Date(created).toISOString(),
4711
- expiresAt: new Date(created + lifetimeMs).toISOString(),
4736
+ createdAt: item.createdAt ?? new Date(created).toISOString(),
4737
+ expiresAt: item.expiresAt ?? new Date(Date.parse(item.createdAt ?? new Date(created).toISOString()) + lifetimeMs).toISOString(),
4712
4738
  initiatorActorId: null,
4713
4739
  decidedByActorId: null,
4714
4740
  decidedAt: null,
@@ -4716,8 +4742,10 @@ class NhAssistantMockProposals {
4716
4742
  executedAt: null,
4717
4743
  rationale: item.rationale ?? null,
4718
4744
  placedBy: item.placedBy ?? null,
4745
+ parent: item.parent ?? null,
4719
4746
  canGiveFeedback: false
4720
4747
  },
4748
+ divisionId: item.divisionId ?? null,
4721
4749
  canDecide: item.canDecide ?? true,
4722
4750
  reasonCode: item.canDecide === false ? item.reasonCode ?? 'assistant-approval-policy-denied' : null
4723
4751
  });
@@ -4728,6 +4756,9 @@ class NhAssistantMockProposals {
4728
4756
  if (segments.length === 2 && segments[1] === 'inbox') {
4729
4757
  return method === 'GET' ? this.inbox(query) : { status: 405 };
4730
4758
  }
4759
+ if (segments.length === 3 && segments[1] === 'inbox' && segments[2] === 'visit') {
4760
+ return method === 'POST' ? this.visit(query) : { status: 405 };
4761
+ }
4731
4762
  if (segments.length === 2 && segments[1] === 'lookup') {
4732
4763
  return method === 'POST' ? this.lookup(body) : { status: 405 };
4733
4764
  }
@@ -4750,6 +4781,12 @@ class NhAssistantMockProposals {
4750
4781
  .reverse();
4751
4782
  return items.length === 0 ? null : items;
4752
4783
  }
4784
+ /** The proposals of a run the caller may see, in the order the scenario lists them. */
4785
+ ofRun(runId) {
4786
+ return this.visible()
4787
+ .filter(item => item.proposal.runId === runId)
4788
+ .map(item => item.proposal);
4789
+ }
4753
4790
  /** The pending proposals on a subject with the caller's right to decide each, earliest expiry first. */
4754
4791
  openOfSubject(subjectType, subjectId) {
4755
4792
  return this.visible()
@@ -4779,19 +4816,49 @@ class NhAssistantMockProposals {
4779
4816
  }
4780
4817
  const agentId = query.get('agentId');
4781
4818
  const runId = query.get('runId');
4819
+ const divisionId = query.get('divisionId');
4820
+ if (divisionId !== null && !guid.test(divisionId)) {
4821
+ return { status: 400 };
4822
+ }
4823
+ const grouped = query.get('grouped') === 'true';
4782
4824
  const items = this.visible()
4783
4825
  .filter(item => item.proposal.status === 'pending' && item.canDecide)
4826
+ .filter(item => divisionId === null || sameGuid(item.divisionId, divisionId))
4784
4827
  .map(item => item.proposal)
4785
4828
  .filter(proposal => (subjectType === null || proposal.subjectType === subjectType)
4786
4829
  && (agentId === null || proposal.agentId === agentId)
4787
4830
  && (runId === null || proposal.runId === runId))
4788
4831
  .sort((left, right) => left.expiresAt.localeCompare(right.expiresAt) || left.approvalId.localeCompare(right.approvalId));
4832
+ const ordered = grouped ? groupedOrder(items) : items;
4833
+ const offset = (page - 1) * itemsPerPage;
4834
+ const pageItems = ordered.slice(offset, offset + itemsPerPage);
4835
+ const scopeKey = scopeKeyOf(divisionId);
4836
+ const lastVisitedAt = this.visits.get(scopeKey) ?? null;
4789
4837
  const body = {
4790
- items: items.slice((page - 1) * itemsPerPage, page * itemsPerPage),
4791
- total: items.length
4838
+ items: pageItems,
4839
+ total: items.length,
4840
+ scopeKey,
4841
+ lastVisitedAt,
4842
+ newApprovalIds: pageItems
4843
+ .filter(proposal => lastVisitedAt === null || Date.parse(proposal.createdAt) > Date.parse(lastVisitedAt))
4844
+ .map(proposal => proposal.approvalId),
4845
+ groups: grouped ? groupsOf(ordered, offset, pageItems.length) : null
4792
4846
  };
4793
4847
  return { status: 200, body };
4794
4848
  }
4849
+ /** `POST approvals/inbox/visit`: the whole inbox, or that of a division the caller works in. */
4850
+ visit(query) {
4851
+ const divisionId = query.get('divisionId');
4852
+ if (divisionId !== null && !guid.test(divisionId)) {
4853
+ return { status: 400 };
4854
+ }
4855
+ if (divisionId !== null && !this.mayWorkIn(divisionId)) {
4856
+ return mockFailure('assistant-subject-not-found');
4857
+ }
4858
+ const body = { scopeKey: scopeKeyOf(divisionId), visitedAt: this.now() };
4859
+ this.visits.set(body.scopeKey, body.visitedAt);
4860
+ return { status: 200, body };
4861
+ }
4795
4862
  read(approvalId) {
4796
4863
  const stored = this.find(approvalId);
4797
4864
  return stored ? { status: 200, body: this.withRight(stored) } : mockFailure('assistant-approval-not-found');
@@ -4879,6 +4946,99 @@ class NhAssistantMockProposals {
4879
4946
  };
4880
4947
  }
4881
4948
  }
4949
+ /** The scope of the inbox's visits: `global`, or the scope key of a division. */
4950
+ function scopeKeyOf(divisionId) {
4951
+ return divisionId === null ? 'global' : `division:${divisionId.replaceAll('-', '').toLowerCase()}`;
4952
+ }
4953
+ function sameGuid(left, right) {
4954
+ return left !== null && left.replaceAll('-', '').toLowerCase() === right.replaceAll('-', '').toLowerCase();
4955
+ }
4956
+ /** The group a proposal belongs to: its parent, or its own subject without one. */
4957
+ function groupKeyOf(proposal) {
4958
+ return proposal.parent
4959
+ ? `${proposal.parent.subjectType}|${proposal.parent.subjectId}`
4960
+ : `${proposal.subjectType}|${proposal.subjectId}`;
4961
+ }
4962
+ function subjectKeyOf(proposal) {
4963
+ return `${groupKeyOf(proposal)}|${proposal.subjectType}|${proposal.subjectId}`;
4964
+ }
4965
+ function firstExpiries(items, key) {
4966
+ const first = new Map();
4967
+ for (const proposal of items) {
4968
+ const current = first.get(key(proposal));
4969
+ if (current === undefined || proposal.expiresAt < current) {
4970
+ first.set(key(proposal), proposal.expiresAt);
4971
+ }
4972
+ }
4973
+ return first;
4974
+ }
4975
+ /**
4976
+ * The order of the grouped inbox, as the server sorts: groups by their first expiry, a group's own
4977
+ * subject first, then its other subjects by their first expiry, one run's proposals together, then by
4978
+ * expiry.
4979
+ */
4980
+ function groupedOrder(items) {
4981
+ const groups = firstExpiries(items, groupKeyOf);
4982
+ const subjects = firstExpiries(items, subjectKeyOf);
4983
+ const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
4984
+ return [...items].sort((left, right) => compare(groups.get(groupKeyOf(left)), groups.get(groupKeyOf(right)))
4985
+ || compare(groupKeyOf(left), groupKeyOf(right))
4986
+ || (left.parent ? 1 : 0) - (right.parent ? 1 : 0)
4987
+ || compare(subjects.get(subjectKeyOf(left)), subjects.get(subjectKeyOf(right)))
4988
+ || compare(subjectKeyOf(left), subjectKeyOf(right))
4989
+ || (left.runId === null ? 1 : 0) - (right.runId === null ? 1 : 0)
4990
+ || compare(left.runId ?? '', right.runId ?? '')
4991
+ || compare(left.expiresAt, right.expiresAt)
4992
+ || compare(left.approvalId, right.approvalId));
4993
+ }
4994
+ /** The groups of the page `ordered[offset, offset + count)`, with their totals and continuation. */
4995
+ function groupsOf(ordered, offset, count) {
4996
+ const page = ordered.slice(offset, offset + count);
4997
+ const previous = offset > 0 ? ordered[offset - 1] : undefined;
4998
+ const next = ordered[offset + count];
4999
+ const groups = [];
5000
+ for (let index = 0; index < page.length;) {
5001
+ const key = groupKeyOf(page[index]);
5002
+ let end = index;
5003
+ while (end < page.length && groupKeyOf(page[end]) === key) {
5004
+ end++;
5005
+ }
5006
+ const members = page.slice(index, end);
5007
+ const own = members.find(proposal => !proposal.parent);
5008
+ const parent = members.find(proposal => proposal.parent)?.parent ?? null;
5009
+ const subjects = [];
5010
+ for (let at = 0; at < members.length;) {
5011
+ const subjectKey = subjectKeyOf(members[at]);
5012
+ let stop = at;
5013
+ while (stop < members.length && subjectKeyOf(members[stop]) === subjectKey) {
5014
+ stop++;
5015
+ }
5016
+ subjects.push({
5017
+ subjectType: members[at].subjectType,
5018
+ subjectId: members[at].subjectId,
5019
+ count: stop - at,
5020
+ total: ordered.filter(proposal => subjectKeyOf(proposal) === subjectKey).length,
5021
+ continuesFromPreviousPage: index === 0 && at === 0 && previous !== undefined && subjectKeyOf(previous) === subjectKey,
5022
+ continuesOnNextPage: end === page.length && stop === members.length && next !== undefined && subjectKeyOf(next) === subjectKey
5023
+ });
5024
+ at = stop;
5025
+ }
5026
+ groups.push({
5027
+ subjectType: own?.subjectType ?? parent.subjectType,
5028
+ subjectId: own?.subjectId ?? parent.subjectId,
5029
+ displayName: own ? own.subjectDisplayName : parent.displayName,
5030
+ subjectTypeName: own ? own.subjectTypeName ?? null : parent.subjectTypeName ?? null,
5031
+ subjectLink: own ? own.subjectLink ?? null : parent.subjectLink ?? null,
5032
+ count: members.length,
5033
+ total: ordered.filter(proposal => groupKeyOf(proposal) === key).length,
5034
+ continuesFromPreviousPage: index === 0 && previous !== undefined && groupKeyOf(previous) === key,
5035
+ continuesOnNextPage: end === page.length && next !== undefined && groupKeyOf(next) === key,
5036
+ subjects
5037
+ });
5038
+ index = end;
5039
+ }
5040
+ return groups;
5041
+ }
4882
5042
  function validDecision(request) {
4883
5043
  return !!request
4884
5044
  && (request.decision === 'approve' || request.decision === 'reject')
@@ -4946,12 +5106,13 @@ class NhAssistantMockBackend {
4946
5106
  runId: proposal.runId,
4947
5107
  subjectType: proposal.subjectType,
4948
5108
  subjectId: proposal.subjectId
4949
- }, proposal.agentId));
5109
+ }, proposal.agentId), (divisionId) => this.subjects.describe(mockSystemSubjectType, divisionSubjectId({ id: divisionId, name: '' }), null, this.admin.administers) != null);
4950
5110
  this.subjects = new NhAssistantMockSubjects(this.scenario.subjectTypes, this.scenario.divisions, (type, id) => this.proposals.openOfSubject(type, id));
4951
5111
  this.agentRuns = new NhAssistantMockAgentRuns(this.scenario, this.subjects, {
4952
5112
  agents: () => this.admin.allAgents(),
4953
5113
  administers: () => this.admin.administers,
4954
5114
  threadProposals: conversationId => this.proposals.ofThread(conversationId),
5115
+ runProposals: runId => this.proposals.ofRun(runId),
4955
5116
  workChanged: (change, agentId) => this.publishAgentWork(change, agentId)
4956
5117
  }, () => new Date().toISOString());
4957
5118
  this.boards = new NhAssistantMockBoards(this.scenario, this.subjects, this.agentRuns, {