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..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()); @@ -1402,6 +1413,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 @@ -2040,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/app/core/models/AgentBuilder.ts b/src/app/core/models/AgentBuilder.ts index 094988fc..8f0f0954 100644 --- a/src/app/core/models/AgentBuilder.ts +++ b/src/app/core/models/AgentBuilder.ts @@ -30,6 +30,15 @@ export interface AgentNode { config_path?: string; isAgentTool?: boolean; skip_summarization?: 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 +65,7 @@ export interface YamlConfig { sub_agents: any; tools?: any[]; callbacks?: any[]; + logging?: LoggingConfig; } export interface DiagramNode { diff --git a/src/utils/yaml-utils.ts b/src/utils/yaml-utils.ts index 1b490c2c..702723bb 100644 --- a/src/utils/yaml-utils.ts +++ b/src/utils/yaml-utils.ts @@ -47,6 +47,23 @@ export class YamlUtils { tools: YamlUtils.buildToolsConfig(agentNode.tools, allTabAgents) } + 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() === '') { delete yamlConfig.description; }