@stack-spot/portal-network 1.0.0-stg.1768944863737 → 1.0.0-stg.1769540429713

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/client/ai.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HttpError } from '@oazapfts/runtime'
2
- import { isArray } from 'lodash'
2
+ import { isArray, isNil } from 'lodash'
3
3
  import { getApiAddresses } from '../api-addresses'
4
4
  import {
5
5
  addFavoriteV1AiStacksStackIdFavoritePost,
@@ -46,9 +46,13 @@ import {
46
46
  postEventV1EventsPost,
47
47
  publishV1QuickCommandsSlugPublishPost,
48
48
  quickActionsV1QuickActionsPost,
49
+ QuickCommandPromptResponse,
49
50
  QuickCommandPromptResponse2,
51
+ QuickCommandScriptExecutionResponse,
50
52
  QuickCommandsExecutionRequest,
51
53
  quickCommandsRunV2V2QuickCommandsSlugStepsStepSlugRunPost,
54
+ QuickCommandStepFetchResponse,
55
+ QuickCommandStepLlmResponse,
52
56
  resetKnowledgeObjectsV1KnowledgeSourcesSlugObjectsDelete,
53
57
  runFetchStepV1QuickCommandsSlugStepsStepSlugFetchRunPost,
54
58
  searchKnowledgeSourcesV1KnowledgeSourcesSearchPost,
@@ -60,17 +64,20 @@ import {
60
64
  updateResourceReviewV1ResourcesResourceTypeSlugResourceSlugReviewsReviewIdPatch,
61
65
  updateReviewCommentV1ResourcesResourceTypeSlugResourceSlugReviewsReviewIdAnswersAnswerIdPatch,
62
66
  updateTitleV1ConversationsConversationIdPatch,
63
- vectorizeCustomKnowledgeSourceV1KnowledgeSourcesSlugCustomPost
67
+ vectorizeCustomKnowledgeSourceV1KnowledgeSourcesSlugCustomPost,
64
68
  } from '../api/ai'
65
69
 
66
-
67
70
  import { StackspotAPIError } from '../error/StackspotAPIError'
68
71
  import { ReactQueryNetworkClient } from '../network/ReactQueryNetworkClient'
69
72
  import { removeAuthorizationParam } from '../utils/remove-authorization-param'
70
73
  import { StreamedJson } from '../utils/StreamedJson'
74
+ import { getSizeOfString } from '../utils/string'
71
75
  import {
72
76
  FixedConversationResponse,
73
77
  FixedDependencyResponse,
78
+ QCContext,
79
+ QCContextExecution,
80
+ QCProgressProps,
74
81
  ReplaceResult,
75
82
  } from './types'
76
83
 
@@ -166,7 +173,7 @@ class AIClient extends ReactQueryNetworkClient {
166
173
  */
167
174
  createQuickCommand = this.mutation(removeAuthorizationParam(createQuickCommandV1QuickCommandsPost))
168
175
  /**
169
- * Creates a QC
176
+ * Deletes a QC
170
177
  */
171
178
  deleteQuickCommand = this.mutation(removeAuthorizationParam(deleteQuickCommandV1QuickCommandsSlugDelete))
172
179
  /**
@@ -367,12 +374,366 @@ class AIClient extends ReactQueryNetworkClient {
367
374
  deleteReviewComment = this.mutation(
368
375
  removeAuthorizationParam(deleteReviewCommentV1ResourcesResourceTypeSlugResourceSlugReviewsReviewIdAnswersAnswerIdDelete))
369
376
 
377
+ /**
378
+ * Runs an Router step of a quick command.
379
+ */
380
+ async runRouterStep(
381
+ ctx: QCContextExecution,
382
+ stepIndex: number, iteration: Record<string, number>,
383
+ progress?:QCProgressProps,
384
+ ) {
385
+ const { qc: { slug, steps }, code, resultMap, customInputs } = ctx
386
+ const step = steps![stepIndex]
387
+ const inputData = Object.keys(customInputs).length > 0 && code ? { ...customInputs, [code]: code } : code ?? customInputs
388
+ try {
389
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, ...{ statusResult: 'START' } })
390
+ if (step.slug in iteration) {
391
+ iteration[step.slug] = iteration[step.slug] + 1
392
+ } else {
393
+ iteration[step.slug] = 1
394
+ }
395
+
396
+ const { next_step_slug } = await aiClient.calculateNextStep.mutate({
397
+ stepSlug: step.slug,
398
+ slug: slug,
399
+ quickCommandEvaluateStepRouterRequest: {
400
+ executions_count: iteration[step.slug],
401
+ input_data: inputData,
402
+ slugs_executions: resultMap,
403
+ },
404
+ })
405
+
406
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, ...{ statusResult: 'END' } })
407
+
408
+ if (next_step_slug === step.slug) {
409
+ return aiClient.runStepsRecursively(stepIndex, ctx, iteration, progress)
410
+ }
411
+ const nextStepIndex = steps?.findIndex((step) => step.slug === next_step_slug)
412
+
413
+ if (isNil(nextStepIndex) || nextStepIndex === -1) return
414
+
415
+ return aiClient.runStepsRecursively(nextStepIndex, ctx, iteration, progress)
416
+ }
417
+ catch (error: any) {
418
+ progress?.onStepChange?.({
419
+ step: step.slug, error: error, answer: JSON.stringify(error.message), statusResult: 'ERROR', ...resultMap,
420
+ })
421
+ // eslint-disable-next-line no-console
422
+ console.error('Error executing QC step', error)
423
+ }
424
+ }
425
+
426
+ async getScriptStepStatus(
427
+ scriptExecutionId: string,
428
+ interval = 5000,
429
+ maxAttempts = 500,
430
+ currentAttempt = 0,
431
+ ): Promise<QuickCommandScriptExecutionResponse> {
432
+ if (currentAttempt >= maxAttempts) {
433
+ throw new Error('Max attempts reached in verify script status')
434
+ }
435
+ await aiClient.getStatusScriptStep.invalidate({ scriptExecutionId })
436
+ const response = await aiClient.getStatusScriptStep.query({ scriptExecutionId })
437
+
438
+ if (response.status === 'success') {
439
+ return response
440
+ }
441
+
442
+ if (response.status === 'failure') {
443
+ throw response
444
+ }
445
+
446
+ await new Promise(resolve => {setTimeout(resolve, interval)})
447
+
448
+ return aiClient.getScriptStepStatus(scriptExecutionId, interval, maxAttempts, currentAttempt + 1)
449
+ }
450
+ /**
451
+ * Runs a fetch step of a quick command and puts the result in the `resultMap` of the context passed as parameter.
452
+ */
453
+ async runFetchStep(ctx: QCContextExecution, stepIndex: number, progress?: QCProgressProps) {
454
+ const { qc: { slug, steps }, code, context, resultMap, customInputs, executionId, signal } = ctx
455
+ const step = steps![stepIndex] as QuickCommandStepFetchResponse
456
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: undefined, statusResult: 'START' })
457
+
458
+ //If is_remote we call backend to execute for us and we only have the response
459
+ if (step.is_remote) {
460
+ ctx.isRemote = true
461
+ try {
462
+ const { data } = await aiClient.fetchStepOfQuickCommandRemotely.mutate({
463
+ slug, stepSlug: step.slug,
464
+ quickCommandsExecutionRequest: {
465
+ code_selection: code, context, qc_execution_id: executionId,
466
+ slugs_executions: { ...resultMap, ...customInputs },
467
+ },
468
+ }, signal)
469
+
470
+ //data is the return of the request in the QC so we do not have full control over the response
471
+ //We handle the usual format with body, status_code and headers, but we might also handle other formats
472
+ const responseData = data as any
473
+ progress?.onStepChange?.({
474
+ step: step.slug, ...resultMap, answer: JSON.stringify(responseData.body) ?? JSON.stringify(responseData), statusResult: 'END' })
475
+ resultMap[step.slug] = {
476
+ status: responseData.status_code || 200,
477
+ data: JSON.stringify(responseData.body) ?? JSON.stringify(responseData),
478
+ headers: responseData.headers ?? {},
479
+ }
480
+ return
481
+ } catch (error) {
482
+ const errorMessage = `${error}, Failed to execute step "${step.slug}" of quick command "${slug}".`
483
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: errorMessage, error: errorMessage, statusResult: 'ERROR' })
484
+ throw new Error(errorMessage)
485
+ }
486
+ }
487
+
488
+ const { headers, data, method, url } = await aiClient.fetchStepOfQuickCommand.mutate({
489
+ slug,
490
+ stepSlug: step.slug,
491
+ quickCommandsExecutionRequest: {
492
+ input_data: code, context, qc_execution_id: executionId,
493
+ slugs_executions: { ...resultMap, ...customInputs },
494
+ },
495
+ }, signal)
496
+ const body = ['get', 'head'].includes(method.toLowerCase()) ? undefined : data
497
+
498
+ try {
499
+ //Local execution
500
+ const response = await fetch(url, { headers: headers || undefined, body, method, signal })
501
+ const responseData = await response.text()
502
+ if (!response.ok) throw new Error(`Failed to execute step "${step.slug}" of quick command "${slug}". Status ${response.status}.`)
503
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: responseData, statusResult: 'END' })
504
+
505
+ resultMap[step.slug] = {
506
+ status: response.status,
507
+ data: responseData,
508
+ headers: Object.fromEntries(response.headers.entries()),
509
+ }
510
+ }
511
+ catch (error) {
512
+ const errorMessage = `${error}, Failed to execute step "${step.slug}" of quick command "${slug}".`
513
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: errorMessage, error: errorMessage, statusResult: 'ERROR' })
514
+ throw new Error(errorMessage)
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Runs an LLM step of a quick command and puts the result in the `resultMap` of the context passed as parameter.
520
+ */
521
+ async runLLMStep(
522
+ { qc: { slug, steps }, code, customInputs, context, executionId, resultMap, signal }: QCContextExecution,
523
+ stepIndex: number,
524
+ progress?: QCProgressProps,
525
+ ) {
526
+ const step = steps![stepIndex] as QuickCommandStepLlmResponse
527
+ let stepContext = context
528
+ if (!step.use_uploaded_files) {
529
+ const { upload_ids: _upload_ids, ...contextDataProps } = context
530
+ stepContext = { ...contextDataProps }
531
+ }
532
+ // eslint-disable-next-line no-async-promise-executor
533
+ return new Promise(async (resolve) => {
534
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: undefined, statusResult: 'START' })
535
+
536
+ const stream = aiClient.streamLlmStepOfQuickCommand(
537
+ slug,
538
+ step.slug,
539
+ {
540
+ input_data: code,
541
+ context: stepContext,
542
+ qc_execution_id: executionId,
543
+ slugs_executions: { ...resultMap, ...customInputs },
544
+ },
545
+ )
546
+
547
+ signal.addEventListener('abort', () => stream.cancel())
548
+
549
+ stream.onChange(item => {
550
+ if (item?.sources?.length) {
551
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, sources: JSON.stringify(item.sources) })
552
+ } else {
553
+ item.answer !== undefined && progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: item.answer })
554
+ }
555
+ })
556
+
557
+ try {
558
+ const finalValue = await stream.getValue()
559
+ resultMap[step.slug] = finalValue
560
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: finalValue.answer,
561
+ sources: finalValue.sources ? JSON.stringify(finalValue.sources) : '', statusResult: 'END' })
562
+ resolve(finalValue)
563
+ } catch (error: any) {
564
+ // eslint-disable-next-line no-console
565
+ console.error('Error executing QC step', error)
566
+ const errorStep = `Failed to execute step "${step.slug}" of quick command "${slug}". Reason: ${error.message}`
567
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, answer: errorStep, error: errorStep, statusResult: 'ERROR' })
568
+ throw error
569
+ }
570
+ })
571
+ }
572
+
573
+ async runScriptStep(
574
+ { qc: { slug, steps }, code, context, resultMap, customInputs, signal }: QCContextExecution,
575
+ stepIndex: number,
576
+ progress?: QCProgressProps,
577
+ ) {
578
+ const step = steps![stepIndex] as QuickCommandStepLlmResponse
579
+ let stepContext = context
580
+ progress?.onStepChange?.({ step: step.slug, ...resultMap, ...{ statusResult: 'START' } })
581
+
582
+ if (!step.use_uploaded_files) {
583
+ const { upload_ids: _upload_ids, ...contextDataProps } = context
584
+ stepContext = { ...contextDataProps }
585
+ }
586
+
587
+ try {
588
+ const { script_execution_id } = await aiClient.startScriptStep.mutate({
589
+ stepSlug: step.slug,
590
+ slug: slug,
591
+ quickCommandStartScriptRequest: {
592
+ input_data: code,
593
+ custom_inputs: customInputs,
594
+ context: stepContext,
595
+ slugs_executions: resultMap,
596
+ },
597
+ }, signal)
598
+ const scriptResult = await aiClient.getScriptStepStatus(script_execution_id)
599
+ progress?.onStepChange?.({ step: step.slug, ...scriptResult, ...{ statusResult: 'END' } })
600
+ resultMap[step.slug] = scriptResult
601
+ }
602
+ catch (error: any) {
603
+ progress?.onStepChange?.({ step: step.slug, ...error, statusResult: 'ERROR', ...resultMap })
604
+ let message = error.result?.error ?? error.message ?? `${error}`
605
+ if (error instanceof StackspotAPIError) message = error.translate()
606
+ throw new Error(`Failed to execute step "${step.slug}" of quick command "${slug}". Error ${message}.`)
607
+ }
608
+ }
609
+
610
+ async runStepsRecursively(currentIndex: number, ctx: QCContextExecution, iteration: Record<string, number>,
611
+ progress?: QCProgressProps) {
612
+ const { qc, resultMap } = ctx
613
+
614
+ if (!qc.steps || currentIndex >= qc.steps?.length) return
615
+ progress?.update?.(currentIndex)
616
+
617
+ const currentStep = qc.steps[currentIndex]
618
+
619
+ if (currentStep.type === 'ROUTER') {
620
+ await aiClient.runRouterStep(ctx, currentIndex, iteration, progress)
621
+ return
622
+ }
623
+
624
+ const parsedStep = currentStep as QuickCommandStepFetchResponse | QuickCommandStepLlmResponse
625
+ let nextIndex = currentIndex + 1
626
+ let nextStepSlug = parsedStep.next_step_slug
627
+
628
+ if (currentStep.type === 'SCRIPT') {
629
+ await aiClient.runScriptStep(ctx, currentIndex, progress)
630
+ } else if (currentStep.type === 'FETCH') {
631
+ await aiClient.runFetchStep(ctx, currentIndex, progress)
632
+ } else {
633
+ try {
634
+ await aiClient.runLLMStep(ctx, currentIndex, progress)
635
+ } catch (error: any) {
636
+ progress?.onStepChange?.({ step: currentStep.slug,
637
+ error: error, answer: JSON.stringify(error), statusResult: 'ERROR', ...resultMap })
638
+ }
639
+ const stepResult = resultMap[currentStep.slug] as QuickCommandPromptResponse
640
+
641
+ //When we have an error but there is an error path defined
642
+ if (typeof stepResult !== 'string' && stepResult.answer_status?.next_step_slug) {
643
+ nextStepSlug = stepResult?.answer_status?.next_step_slug
644
+ } else if (!stepResult?.answer_status?.success) { //When we have an error but no error path defined we should fail the execution
645
+ progress?.onStepChange?.({
646
+ step: currentStep.slug, error: stepResult?.answer_status,
647
+ answer: JSON.stringify(stepResult?.answer_status?.failure_message), statusResult: 'ERROR', ...resultMap })
648
+ throw new Error()
649
+ }
650
+ }
651
+
652
+ const stepResult = ctx.resultMap[currentStep.slug]
653
+ if (stepResult && typeof stepResult !== 'string' && 'answer_status' in stepResult && !!stepResult.answer_status?.next_step_slug) {
654
+ nextStepSlug = stepResult.answer_status.next_step_slug
655
+ }
656
+
657
+ if (nextStepSlug) {
658
+ nextIndex = nextStepSlug === 'end' ?
659
+ qc.steps.length : qc.steps?.findIndex((step) => step.slug === nextStepSlug)
660
+ }
661
+ await aiClient.runStepsRecursively(nextIndex, ctx, iteration, progress)
662
+ }
663
+
664
+ async formatResult({ qc, code, executionId, context, resultMap, customInputs, signal }: QCContextExecution) {
665
+ const formatted = await aiClient.formatResultOfQuickCommand.mutate({
666
+ slug: qc.slug,
667
+ quickCommandsExecutionRequest: {
668
+ input_data: code,
669
+ context,
670
+ qc_execution_id: executionId,
671
+ slugs_executions: { ...resultMap, ...customInputs },
672
+ },
673
+
674
+ }, signal)
675
+ return formatted.result
676
+ }
677
+
678
+ /**
679
+ * This registers a quick command event in the backend (analytics).
680
+ */
681
+ private async registerQCAnalyticsEvent({
682
+ qc, isRemote, executionId, code = '', context }: QCContextExecution, status: string, start: number) {
683
+ const now = new Date().getTime()
684
+ try {
685
+ await aiClient.createEvent.mutate({
686
+ body: [{
687
+ type: 'custom_quick_command_execution',
688
+ quick_command_event: {
689
+ type: qc.type || '',
690
+ duration_execution: now - start,
691
+ status_execution: status,
692
+ slug: qc.slug,
693
+ qc_execution_id: executionId,
694
+ id: qc.id,
695
+ is_remote: isRemote,
696
+ },
697
+ code,
698
+ context,
699
+ knowledge_sources: [],
700
+ size: getSizeOfString(code),
701
+ generated_at: now,
702
+ }],
703
+ })
704
+ } catch (error) {
705
+ // eslint-disable-next-line no-console
706
+ console.warn('Failed to register event: quick command.')
707
+ }
708
+ }
709
+
710
+ async runQuickCommand(ctx: QCContext, progress?: QCProgressProps) {
711
+ const start = new Date().getTime()
712
+
713
+ const { slug } = ctx
714
+ const qc = await aiClient.quickCommand.query({ slug })
715
+ const ctxExecution: QCContextExecution = { ...ctx, qc }
716
+ try {
717
+ await aiClient.runStepsRecursively(0, ctxExecution, {}, progress)
718
+ progress?.remove?.()
719
+ const result = await aiClient.formatResult(ctxExecution)
720
+ await aiClient.registerQCAnalyticsEvent(ctxExecution, '200', start)
721
+ return result
722
+ } catch (error: any) {
723
+ let message = error.message || `${error}`
724
+ if (error instanceof StackspotAPIError) message = error.translate()
725
+ await aiClient.registerQCAnalyticsEvent(ctxExecution, message, start)
726
+ throw error
727
+ }
728
+ }
729
+
370
730
  contentDependencies = this.query(removeAuthorizationParam(
371
731
  getContentDependenciesV1ContentContentTypeContentIdDependenciesGet as ReplaceResult<
372
732
  typeof getContentDependenciesV1ContentContentTypeContentIdDependenciesGet,
373
733
  FixedDependencyResponse
374
734
  >,
375
735
  ))
736
+
376
737
  }
377
738
 
378
739
  export const aiClient = new AIClient()
@@ -1,10 +1,9 @@
1
1
  import { RequestOpts } from '@oazapfts/runtime'
2
2
  import { AccountScmInfoSaveRequest, AccountScmInfoUpdateRequest, AccountScmStatusResponse, GroupsFromResourceResponse, MembersFromResourceResponse } from '../api/account'
3
3
  import { AgentVisibilityLevelEnum, HttpMethod, ListAgentResponse, VisibilityLevelEnum } from '../api/agent-tools'
4
- import { ChatResponse3, ContentDependencyResponse, ConversationHistoryResponse, ConversationResponse, DependencyResponse, SourceKnowledgeSource, SourceProjectFile3, SourceStackAi } from '../api/ai'
4
+ import { ChatRequest, ChatResponse3, ContentDependencyResponse, ConversationHistoryResponse, ConversationResponse, DependencyResponse, QuickCommandResponse, QuickCommandStepResult, SourceKnowledgeSource, SourceProjectFile3, SourceStackAi } from '../api/ai'
5
5
  import { ConnectAccountRequestV2, ManagedAccountProvisionRequest } from '../api/cloudAccount'
6
6
  import { AllocationCostRequest, AllocationCostResponse, ChargePeriod, getAllocationCostFilters, ManagedService, ServiceResource } from '../api/cloudServices'
7
- import { ChatRequest } from '../api/genAiInference'
8
7
  import { Action } from '../api/workspace-ai'
9
8
  import { ActivityResponse, FullInputContextResponse, InputConditionResponse, InputValuesContextResponse, PaginatedActivityResponse, PluginForAppCreationV2Response, PluginInputValuesInConsolidatedContextResponse, ValueByEnvResponse, WorkflowForCreationResponse } from '../api/workspaceManager'
10
9
 
@@ -178,6 +177,7 @@ export interface FixedChatRequest extends ChatRequest {
178
177
  stackspot_ai_version?: string,
179
178
  os?: string,
180
179
  agent_id?: string,
180
+ upload_ids?: string[],
181
181
  },
182
182
  }
183
183
 
@@ -421,3 +421,32 @@ export interface AgentToolsOpenAPIPreview {
421
421
  parameters?: Record<string, any>,
422
422
  request_body?: Record<string, any>,
423
423
  }
424
+
425
+ type SlugExecution = Record<string, QuickCommandStepResult>
426
+
427
+
428
+ interface BaseQCContext {
429
+ context: Required<FixedChatRequest>['context'],
430
+ resultMap: SlugExecution,
431
+ customInputs: Record<string, string>,
432
+ code?: string,
433
+ executionId: string,
434
+ signal: AbortSignal,
435
+ isRemote?: boolean,
436
+ headers?: Record<string, string>,
437
+ conversation_id?: string,
438
+ }
439
+
440
+ export interface QCContext extends BaseQCContext {
441
+ slug: string,
442
+ }
443
+
444
+ export interface QCContextExecution extends BaseQCContext {
445
+ qc: QuickCommandResponse,
446
+ }
447
+
448
+ export interface QCProgressProps{
449
+ update?: (index: number) => void,
450
+ remove?: () => void,
451
+ onStepChange?: (stepResult: any) => void,
452
+ }
@@ -17,3 +17,15 @@ export function formatJson(data: any): string {
17
17
  }
18
18
  return JSON.stringify(data, null, 2)
19
19
  }
20
+
21
+
22
+ /**
23
+ * Gets the size of a string removing control characters and spaces
24
+ * @param str the string to count.
25
+ * @returns the count value.
26
+ */
27
+ export function getSizeOfString(str: string): number {
28
+ // eslint-disable-next-line no-control-regex
29
+ const withoutSpacesAndControls = str.replace(/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069\s]/g, '')
30
+ return withoutSpacesAndControls.length
31
+ }