@stack-spot/portal-network 0.201.0-beta.1 → 0.202.0-beta.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.
package/src/client/ai.ts CHANGED
@@ -64,6 +64,7 @@ import { StreamedJson } from '../utils/StreamedJson'
64
64
  import { formatJson } from '../utils/string'
65
65
  import { agentToolsClient } from './agent-tools'
66
66
  import {
67
+ AgentInfo,
67
68
  ChatAgentTool,
68
69
  ChatResponseWithSteps,
69
70
  FixedChatRequest,
@@ -367,6 +368,10 @@ class AIClient extends ReactQueryNetworkClient {
367
368
  { method: 'post', body: JSON.stringify(request), headers, signal: abortController.signal },
368
369
  )
369
370
 
371
+ const DYNAMIC_TOOL_ID = 'dynamic'
372
+ function isDynamicTool(info: AgentInfo) {
373
+ return info.type === 'tool' && info.id === DYNAMIC_TOOL_ID
374
+ }
370
375
  /**
371
376
  * This function treats events in the streaming that deals with the execution of tools. Since these events are not concatenated like
372
377
  * normal streamings of data, we need this separate function to deal with them. It transforms the internal data model of the
@@ -374,12 +379,10 @@ class AIClient extends ReactQueryNetworkClient {
374
379
  */
375
380
  async function transform(event: Partial<FixedChatResponse>, data: Partial<ChatResponseWithSteps>) {
376
381
  const info = event.agent_info
377
-
378
382
  if (!info) return
379
-
380
383
  const tools = await AIClient.toolsOfAgent(request.context?.agent_id)
381
384
  data.steps = data.steps ? [...data.steps] : []
382
-
385
+
383
386
  if (info.type === 'planning' && info.action === 'end') {
384
387
  data.steps.push({
385
388
  id: 'planning',
@@ -448,6 +451,39 @@ class AIClient extends ReactQueryNetworkClient {
448
451
  }
449
452
  }
450
453
 
454
+ if (info.type === 'tool_calls' && info.action === 'start') {
455
+ const hasPlanning = data.steps.find(s => s.type === 'planning')
456
+ // On the first tool_calls:start, create the synthetic planning ("dynamic") step.
457
+ if (!hasPlanning) {
458
+ const userPrompt = request.user_prompt === 'string' ? request.user_prompt : JSON.stringify(request.user_prompt)
459
+ data.steps.push({
460
+ id: 'dynamic',
461
+ type: 'planning',
462
+ status: 'success',
463
+ steps: [],
464
+ goal: userPrompt,
465
+ user_question: userPrompt,
466
+ })
467
+ }
468
+ const toolsStepId = data.steps.filter(s => s.id === 'tools' || s.id.startsWith('tools-')).length + 1
469
+ data.steps.push({
470
+ id: `tools-${toolsStepId.toString()}`,
471
+ type: 'step',
472
+ status: 'running',
473
+ attempts: [{ tools: [] }],
474
+ } as StepChatStep)
475
+ }
476
+
477
+ if (info.type === 'tool_calls' && info.action === 'end') {
478
+ const lastStep = findLast(data.steps, s => s.id === 'tools' || s.id.startsWith('tools-')) as StepChatStep
479
+ if (lastStep) {
480
+ lastStep.status = 'success'
481
+ lastStep.duration = info.duration
482
+ const lastAttemptOfLastTool = last(lastStep.attempts.map(a => a.tools).flat())
483
+ lastStep.output = lastAttemptOfLastTool?.output
484
+ }
485
+ }
486
+
451
487
  if (info.type === 'tool' && info.action === 'awaiting_approval') {
452
488
  const tool = tools.find(({ id }) => id === info.data?.tool_id)
453
489
  data.steps.push({
@@ -471,13 +507,14 @@ class AIClient extends ReactQueryNetworkClient {
471
507
  }
472
508
 
473
509
  if (info.type === 'tool' && info.action === 'start') {
474
- const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
475
510
  if (!info.data) return
511
+ const input = formatJson(info.data.input)
512
+ const tool = findLast(tools, ({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
513
+
514
+ const currentStep = findLast(data.steps, s => s.status === 'running') as StepChatStep
476
515
 
477
516
  //There might be a tool with status awaiting_approval, so we want to inform tool has already started
478
- if (!currentStep || !currentStep.attempts[0].tools) {
479
- const input = formatJson(info.data.input)
480
- const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
517
+ if (!currentStep || !currentStep?.attempts?.[0]?.tools) {
481
518
  data.steps.push({
482
519
  id: info.id,
483
520
  type: 'tool',
@@ -490,23 +527,23 @@ class AIClient extends ReactQueryNetworkClient {
490
527
  }],
491
528
  })
492
529
  } else {
493
- const toolInFirstAttempt = currentStep.attempts[0].tools?.find(t => t.executionId === info.id)
530
+ const toolInFirstAttempt = findLast(currentStep?.attempts?.[0]?.tools, t => t.executionId === info.id)
494
531
  //One step might have multiple tools. When in an approval mode, we might not have all the tools in the array yet.
495
- //So we make sure to add any tools that are not in there.
496
- if (!toolInFirstAttempt) {
497
- const input = formatJson(info.data.input)
498
- const tool = tools?.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
499
- currentStep.attempts[info.data.attempt - 1].tools?.push({
532
+ //For dynamic tools (id === 'dynamic'), we always push a new tool, since dynamic executions can trigger
533
+ //multiple tool runs in the same step and do not follow the planned tool structure.
534
+ //So we make sure to add any tools that are not in there, or always add for dynamic tools.
535
+ if (!toolInFirstAttempt || isDynamicTool(info)) {
536
+ currentStep.attempts?.[0].tools?.push({
500
537
  ...tool,
501
538
  executionId: info.id,
502
539
  input,
540
+ status: 'running',
503
541
  })
504
542
  } else {
505
543
  const input = formatJson(info.data.input)
506
544
  if (info.data.attempt === 1) {
507
545
  toolInFirstAttempt.input = input
508
546
  } else {
509
- const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
510
547
  currentStep.attempts[info.data.attempt - 1] ??= { tools: [] }
511
548
  currentStep.attempts[info.data.attempt - 1].tools?.push({
512
549
  ...tool,
@@ -521,10 +558,14 @@ class AIClient extends ReactQueryNetworkClient {
521
558
  if (info.type === 'tool' && info.action === 'end') {
522
559
  const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
523
560
  if (!currentStep || !info.data) return
524
- const tool = currentStep.attempts[info.data.attempt - 1]?.tools?.find(t => t.executionId === info.id)
561
+
562
+ // attempt index for tool execution starts at 0 for dynamically executed tools,while for planned tools it starts at 1
563
+ const attempt = isDynamicTool(info) ? info.data.attempt : info.data.attempt - 1
564
+ const tool = last(currentStep?.attempts?.[attempt]?.tools)
525
565
  if (tool) {
526
566
  tool.output = formatJson(info.data.output)
527
567
  tool.duration = info.duration
568
+ tool.status = 'success'
528
569
  }
529
570
  }
530
571
 
@@ -533,11 +574,14 @@ class AIClient extends ReactQueryNetworkClient {
533
574
  if (answerStep) answerStep.status = 'running'
534
575
  }
535
576
 
577
+
536
578
  if (info.type === 'chat' && info.action === 'end') {
537
- const answerStep = last(data.steps)
538
- if (answerStep) {
539
- answerStep.status = 'success'
540
- answerStep.duration = info.duration
579
+ const lastStep = last(data.steps)
580
+ if (lastStep?.type === 'answer') {
581
+ lastStep.status = 'success'
582
+ lastStep.duration = info.duration
583
+ } else {
584
+ data.steps.push({ id: 'answer', type: 'answer', status: 'success' })
541
585
  }
542
586
  }
543
587
  }
@@ -247,6 +247,7 @@ export interface ChatAgentTool {
247
247
  input?: string,
248
248
  output?: string,
249
249
  goal?: string,
250
+ status?: 'running' | 'success' | 'error',
250
251
  }
251
252
 
252
253
  export interface ChatStepAttempt {
@@ -308,10 +309,10 @@ export interface AnswerChatStep extends BaseChatStep {
308
309
  export type ChatStep = PlanningChatStep | StepChatStep | AnswerChatStep | ToolChatStep
309
310
 
310
311
  export interface BaseAgentInfo {
311
- type: 'chat' | 'planning' | 'step' | 'tool' | 'final_answer',
312
+ type: 'chat' | 'planning' | 'step' | 'tool' | 'final_answer' | 'tool_calls',
312
313
  action: 'start' | 'end' | 'awaiting_approval',
313
314
  duration?: number,
314
- id: string,
315
+ id?: string,
315
316
  }
316
317
 
317
318
  export interface AgentTool {
@@ -355,7 +356,15 @@ export interface GenericAgentInfo extends BaseAgentInfo {
355
356
  type: 'chat' | 'final_answer',
356
357
  }
357
358
 
358
- export type AgentInfo = GenericAgentInfo | PlanningAgentInfo | StepAgentInfo | ToolAgentInfo
359
+ export interface ToolCallsAgentInfo extends BaseAgentInfo {
360
+ type: 'tool_calls',
361
+ data?: {
362
+ tools?: string[],
363
+ duration?: number,
364
+ },
365
+ }
366
+
367
+ export type AgentInfo = GenericAgentInfo | PlanningAgentInfo | StepAgentInfo | ToolAgentInfo | ToolCallsAgentInfo
359
368
 
360
369
  export interface FixedChatResponse extends ChatResponse3 {
361
370
  agent_info: AgentInfo,
@@ -1,4 +1,4 @@
1
- import { AuthenticationError, SessionExpiredError } from '@stack-spot/auth'
1
+ import { AuthenticationError, Session, SessionExpiredError } from '@stack-spot/auth'
2
2
  import { requestPermission, setup as setupPermissions } from '@stack-spot/opa'
3
3
  import { events } from 'fetch-event-stream'
4
4
  import { getApisBaseUrlConfig, getBaseUrlByTenantWithOverride, getPermissionsAPIMap } from '../api-addresses'
@@ -44,12 +44,12 @@ export abstract class NetworkClient {
44
44
  NetworkClient.tenant = tenant
45
45
  const url = getApisBaseUrlConfig(env, tenant).permissionValidation
46
46
  const apiMap = getPermissionsAPIMap(env, tenant)
47
- sessionManager.onChange?.((session) => {
48
- if (session) {
49
- queryClient.invalidateQueries()
50
- setupPermissions({ url, session, apiMap })
51
- }
52
- })
47
+ const onChange = (session?: Session) => {
48
+ queryClient.invalidateQueries()
49
+ session && setupPermissions({ url, session, apiMap })
50
+ }
51
+ sessionManager.onChange?.(onChange)
52
+ if (sessionManager.hasSession()) onChange(sessionManager.getSession())
53
53
  }
54
54
 
55
55
  /**