From ecd4dfd4b8f0b1f0ab4098e54e434d7d4c5f6a29 Mon Sep 17 00:00:00 2001 From: Huan Chen Date: Fri, 6 Mar 2026 02:46:39 +0000 Subject: [PATCH 1/3] feat(builder): add BigQuery integration for agent analytics Adds a new section to the Agent Builder that allows users to easily enable and configure interaction logging to Google BigQuery. --- .../builder-tabs/builder-tabs.component.html | 49 ++++++++++++++++++ .../builder-tabs/builder-tabs.component.scss | 50 +++++++++++++++++++ .../builder-tabs/builder-tabs.component.ts | 24 +++++++++ src/app/components/canvas/canvas.component.ts | 7 +++ src/app/core/models/AgentBuilder.ts | 12 +++++ src/utils/yaml-utils.ts | 13 +++++ 6 files changed, 155 insertions(+) diff --git a/src/app/components/builder-tabs/builder-tabs.component.html b/src/app/components/builder-tabs/builder-tabs.component.html index abe9f09a..f22e3093 100644 --- a/src/app/components/builder-tabs/builder-tabs.component.html +++ b/src/app/components/builder-tabs/builder-tabs.component.html @@ -101,6 +101,55 @@ } + @if (agentConfig.isRoot) { +
+ + Enable logging to BigQuery + + help_outline +
+ + @if (agentConfig.logging?.enabled) { +
+
+ Logging to BigQuery +
+

+ Agent Analytics (powered by BigQuery): Please provide the dataset and table information to store interaction data. If the Table ID is left empty, the default table 'agent_events_v2' will be used. If the target table already exists, it will be used for logging (ensure you have table write permissions); otherwise, it will be created automatically (requires dataset write permissions). + Learn more +

+
+ + Project ID * + + + + Dataset ID * + + +
+
+ + Table ID (optional) + + + + Dataset Location * + + +
+
+ } + } + @if (agentConfig.agent_class === 'LoopAgent') { Max Iteration diff --git a/src/app/components/builder-tabs/builder-tabs.component.scss b/src/app/components/builder-tabs/builder-tabs.component.scss index 4b2cdf3a..b51b1e4f 100644 --- a/src/app/components/builder-tabs/builder-tabs.component.scss +++ b/src/app/components/builder-tabs/builder-tabs.component.scss @@ -138,6 +138,56 @@ margin-bottom: 8px; } + .analytics-hint { + margin: 0 0 16px 0; + font-size: 13px; + line-height: 1.5; + color: var(--builder-text-secondary-color); + + .learn-more-link { + color: var(--builder-text-link-color); + text-decoration: none; + display: inline-block; + margin-top: 4px; + font-weight: 500; + + &:hover { + text-decoration: underline; + } + } + } + + .logging-checkbox-row { + display: flex; + align-items: center; + gap: 4px; + margin-top: 16px; + margin-bottom: 8px; + + .logging-help-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #c4c7c5; + cursor: help; + } + } + + .analytics-config-section { + margin-top: 8px; + padding: 16px; + border: 1px solid var(--builder-border-color); + border-radius: 8px; + background-color: var(--mat-sys-surface-container-low); + + .logging-section-title { + font-weight: 500; + margin-bottom: 12px; + font-size: 14px; + color: var(--mat-sys-on-surface); + } + } + .tool-code-section { margin-top: 16px; diff --git a/src/app/components/builder-tabs/builder-tabs.component.ts b/src/app/components/builder-tabs/builder-tabs.component.ts index 93571e7b..9d9b6cec 100644 --- a/src/app/components/builder-tabs/builder-tabs.component.ts +++ b/src/app/components/builder-tabs/builder-tabs.component.ts @@ -708,6 +708,19 @@ export class BuilderTabsComponent { } } + onTelemetryChange(enabled: boolean) { + if (this.agentConfig) { + if (!this.agentConfig.logging) { + this.agentConfig.logging = { + enabled: enabled, + dataset_location: 'US' + }; + } else { + this.agentConfig.logging.enabled = enabled; + } + } + } + createAgentTool() { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { width: '750px', @@ -734,6 +747,17 @@ export class BuilderTabsComponent { } saveChanges() { + if (this.agentConfig?.isRoot && this.agentConfig?.logging?.enabled) { + if (!this.agentConfig.logging.project_id?.trim() || + !this.agentConfig.logging.dataset_id?.trim() || + !this.agentConfig.logging.dataset_location?.trim()) { + this.snackBar.open("Project ID, Dataset ID, and Dataset Location are required when Agent Analytics is enabled.", "OK", { + duration: 3000 + }); + return; + } + } + const rootAgent = this.agentBuilderService.getRootNode(); if (!rootAgent) { diff --git a/src/app/components/canvas/canvas.component.ts b/src/app/components/canvas/canvas.component.ts index be573e8c..ae253621 100644 --- a/src/app/components/canvas/canvas.component.ts +++ b/src/app/components/canvas/canvas.component.ts @@ -1402,6 +1402,13 @@ export class CanvasComponent implements AfterViewInit, OnInit, OnChanges { sub_agents: yamlData.sub_agents || [], tools: this.parseToolsFromYaml(yamlData.tools || []), callbacks: this.parseCallbacksFromYaml(yamlData), + logging: yamlData.logging ? { + enabled: true, + project_id: yamlData.logging.project_id, + dataset_id: yamlData.logging.dataset_id, + table_id: yamlData.logging.table_id, + dataset_location: yamlData.logging.dataset_location, + } : undefined }; // Add to agent builder service diff --git a/src/app/core/models/AgentBuilder.ts b/src/app/core/models/AgentBuilder.ts index 094988fc..ed0e2e8e 100644 --- a/src/app/core/models/AgentBuilder.ts +++ b/src/app/core/models/AgentBuilder.ts @@ -30,6 +30,16 @@ export interface AgentNode { config_path?: string; isAgentTool?: boolean; skip_summarization?: boolean; + visual_builder?: boolean; + logging?: LoggingConfig; +} + +export interface LoggingConfig { + enabled?: boolean; + project_id?: string; + dataset_id?: string; + table_id?: string; + dataset_location?: string; } export interface ToolNode { @@ -56,6 +66,8 @@ export interface YamlConfig { sub_agents: any; tools?: any[]; callbacks?: any[]; + visual_builder?: boolean; + logging?: LoggingConfig; } export interface DiagramNode { diff --git a/src/utils/yaml-utils.ts b/src/utils/yaml-utils.ts index 1b490c2c..2db89999 100644 --- a/src/utils/yaml-utils.ts +++ b/src/utils/yaml-utils.ts @@ -47,6 +47,19 @@ export class YamlUtils { tools: YamlUtils.buildToolsConfig(agentNode.tools, allTabAgents) } + if (agentNode.isRoot) { + yamlConfig.visual_builder = true; + if (agentNode.logging?.enabled) { + const logging = agentNode.logging!; + yamlConfig.logging = { + project_id: logging.project_id, + dataset_id: logging.dataset_id, + table_id: logging.table_id, + dataset_location: logging.dataset_location + }; + } + } + if (!agentNode.description || agentNode.description.trim() === '') { delete yamlConfig.description; } From 393ec8f78f78df871785322978e9476d5cd48c2e Mon Sep 17 00:00:00 2001 From: Huan Chen Date: Thu, 19 Mar 2026 17:59:03 +0000 Subject: [PATCH 2/3] remove visual builder flag --- src/app/core/models/AgentBuilder.ts | 2 -- src/utils/yaml-utils.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/app/core/models/AgentBuilder.ts b/src/app/core/models/AgentBuilder.ts index ed0e2e8e..8f0f0954 100644 --- a/src/app/core/models/AgentBuilder.ts +++ b/src/app/core/models/AgentBuilder.ts @@ -30,7 +30,6 @@ export interface AgentNode { config_path?: string; isAgentTool?: boolean; skip_summarization?: boolean; - visual_builder?: boolean; logging?: LoggingConfig; } @@ -66,7 +65,6 @@ export interface YamlConfig { sub_agents: any; tools?: any[]; callbacks?: any[]; - visual_builder?: boolean; logging?: LoggingConfig; } diff --git a/src/utils/yaml-utils.ts b/src/utils/yaml-utils.ts index 2db89999..c2cb4376 100644 --- a/src/utils/yaml-utils.ts +++ b/src/utils/yaml-utils.ts @@ -48,7 +48,6 @@ export class YamlUtils { } if (agentNode.isRoot) { - yamlConfig.visual_builder = true; if (agentNode.logging?.enabled) { const logging = agentNode.logging!; yamlConfig.logging = { From df70672e53f2bcd5e0f30f61d0551d879c18e001 Mon Sep 17 00:00:00 2001 From: Huan Chen Date: Thu, 2 Apr 2026 05:01:07 +0000 Subject: [PATCH 3/3] update saving logic, add plugins.yaml --- src/app/components/canvas/canvas.component.ts | 30 ++++++++++++++----- src/app/components/chat/chat.component.ts | 15 ++++++---- src/utils/yaml-utils.ts | 17 +++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/app/components/canvas/canvas.component.ts b/src/app/components/canvas/canvas.component.ts index ae253621..95935a5c 100644 --- a/src/app/components/canvas/canvas.component.ts +++ b/src/app/components/canvas/canvas.component.ts @@ -27,8 +27,8 @@ import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu"; import * as YAML from 'yaml'; -import { firstValueFrom, Observable } from "rxjs"; -import { take, filter } from "rxjs/operators"; +import { firstValueFrom, Observable, forkJoin, of } from "rxjs"; +import { take, filter, catchError } from "rxjs/operators"; import { YamlUtils } from "../../../utils/yaml-utils"; import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; import { AddToolDialogComponent } from "../add-tool-dialog/add-tool-dialog.component"; @@ -1377,11 +1377,22 @@ export class CanvasComponent implements AfterViewInit, OnInit, OnChanges { return toolsMap.get(nodeName) ?? []; } - loadFromYaml(yamlContent: string, appName: string) { + loadFromYaml(yamlContent: string, appName: string, pluginsContent?: string) { try { // Parse the YAML content const yamlData = YAML.parse(yamlContent); + if (pluginsContent) { + try { + const pluginsData = YAML.parse(pluginsContent); + if (pluginsData && pluginsData.bigquery_agent_analytics) { + yamlData.logging = pluginsData.bigquery_agent_analytics; + } + } catch (e) { + // It's fine if plugins.yaml is not valid YAML or doesn't exist + } + } + this.agentBuilderService.clear(); this.nodePositions.clear(); this.agentToolBoards.set(new Map()); @@ -2047,10 +2058,15 @@ export class CanvasComponent implements AfterViewInit, OnInit, OnChanges { reloadCanvasFromYaml(): void { if (this.appNameInput) { - this.agentService.getAgentBuilderTmp(this.appNameInput).subscribe({ - next: (yamlContent: string) => { - if (yamlContent) { - this.loadFromYaml(yamlContent, this.appNameInput); + const rootYaml$ = this.agentService.getAgentBuilderTmp(this.appNameInput); + const pluginsYaml$ = this.agentService.getSubAgentBuilder(this.appNameInput, 'plugins.yaml').pipe( + catchError(() => of('')) + ); + + forkJoin([rootYaml$, pluginsYaml$]).subscribe({ + next: ([rootContent, pluginsContent]) => { + if (rootContent) { + this.loadFromYaml(rootContent, this.appNameInput, pluginsContent); } }, error: (error) => { diff --git a/src/app/components/chat/chat.component.ts b/src/app/components/chat/chat.component.ts index e833fc55..17adc601 100644 --- a/src/app/components/chat/chat.component.ts +++ b/src/app/components/chat/chat.component.ts @@ -33,7 +33,7 @@ import {MatTooltip} from '@angular/material/tooltip'; import {SafeHtml} from '@angular/platform-browser'; import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; import {NgxJsonViewerModule} from 'ngx-json-viewer'; -import {BehaviorSubject, combineLatest, Observable, of} from 'rxjs'; +import {BehaviorSubject, combineLatest, forkJoin, Observable, of} from 'rxjs'; import {catchError, distinctUntilChanged, filter, first, map, shareReplay, switchMap, take, tap} from 'rxjs/operators'; import {URLUtil} from '../../../utils/url-util'; @@ -1927,10 +1927,15 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { private loadExistingAgentConfiguration() { - this.agentService.getAgentBuilderTmp(this.appName).subscribe({ - next: (yamlContent: string) => { - if (yamlContent) { - this.canvasComponent()?.loadFromYaml(yamlContent, this.appName); + const rootYaml$ = this.agentService.getAgentBuilderTmp(this.appName); + const pluginsYaml$ = this.agentService.getSubAgentBuilder(this.appName, 'plugins.yaml').pipe( + catchError(() => of('')) + ); + + forkJoin([rootYaml$, pluginsYaml$]).subscribe({ + next: ([rootContent, pluginsContent]) => { + if (rootContent) { + this.canvasComponent()?.loadFromYaml(rootContent, this.appName, pluginsContent); } }, error: (error: any) => { diff --git a/src/utils/yaml-utils.ts b/src/utils/yaml-utils.ts index c2cb4376..702723bb 100644 --- a/src/utils/yaml-utils.ts +++ b/src/utils/yaml-utils.ts @@ -47,16 +47,21 @@ export class YamlUtils { tools: YamlUtils.buildToolsConfig(agentNode.tools, allTabAgents) } - if (agentNode.isRoot) { - if (agentNode.logging?.enabled) { - const logging = agentNode.logging!; - yamlConfig.logging = { + if (agentNode.isRoot && agentNode.logging?.enabled) { + const logging = agentNode.logging!; + const pluginsConfig = { + bigquery_agent_analytics: { project_id: logging.project_id, dataset_id: logging.dataset_id, table_id: logging.table_id, dataset_location: logging.dataset_location - }; - } + } + }; + const pluginsYamlString = YAML.stringify(pluginsConfig); + const pluginsBlob = new Blob([pluginsYamlString], { type: 'application/x-yaml' }); + const pluginsFileName = `${appName}/plugins.yaml`; + const pluginsFile = new File([pluginsBlob], pluginsFileName, { type: 'application/x-yaml' }); + formData.append('files', pluginsFile); } if (!agentNode.description || agentNode.description.trim() === '') {