From 2147b8454d6c4076d447c2fad73dbdddb638d4db Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Fri, 22 May 2026 12:24:57 +0000 Subject: [PATCH] edit-schema: use backend preview endpoint for diagram instead of client-side SQL parsing Replace the frontend node-sql-parser AST walk that built the AI-change diagram preview with a call to POST /connection/diagram/:id/preview, so the preview matches what will actually be applied and overlays proposed changes on the real schema. Also bumps pnpm audit overrides. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../edit-database-schema.component.ts | 265 ++++++------------ .../src/app/services/table-schema.service.ts | 43 ++- package.json | 9 +- pnpm-lock.yaml | 49 ++-- 4 files changed, 165 insertions(+), 201 deletions(-) diff --git a/frontend/src/app/components/edit-database-schema/edit-database-schema.component.ts b/frontend/src/app/components/edit-database-schema/edit-database-schema.component.ts index 98717f98a..608e2dc54 100644 --- a/frontend/src/app/components/edit-database-schema/edit-database-schema.component.ts +++ b/frontend/src/app/components/edit-database-schema/edit-database-schema.component.ts @@ -1,15 +1,15 @@ -import { Component, EventEmitter, Input, OnInit, Output, signal, inject, computed } from '@angular/core'; +import { TextFieldModule } from '@angular/cdk/text-field'; import { CommonModule } from '@angular/common'; +import { Component, computed, EventEmitter, Input, inject, OnInit, Output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { TextFieldModule } from '@angular/cdk/text-field'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Parser } from 'node-sql-parser'; -import { TableSchemaService, SchemaChangeResponse } from 'src/app/services/table-schema.service'; +import { SchemaChangeResponse, TableSchemaService } from 'src/app/services/table-schema.service'; import { SchemaDiagramViewerComponent } from './schema-diagram-viewer/schema-diagram-viewer.component'; interface ParsedColumn { @@ -95,9 +95,7 @@ export class EditDatabaseSchemaComponent implements OnInit { return null; }); - protected chatMessages = computed(() => - this.messages().filter(m => m.role !== 'diagram'), - ); + protected chatMessages = computed(() => this.messages().filter((m) => m.role !== 'diagram')); ngOnInit(): void { if (!this.connectionID) { @@ -121,7 +119,7 @@ export class EditDatabaseSchemaComponent implements OnInit { const prompt = this.userPrompt().trim(); if (!prompt || this.submitting()) return; - this.messages.update(msgs => [...msgs, { role: 'user', text: prompt }]); + this.messages.update((msgs) => [...msgs, { role: 'user', text: prompt }]); this.userPrompt.set(''); this.submitting.set(true); @@ -132,16 +130,28 @@ export class EditDatabaseSchemaComponent implements OnInit { } if (result && result.changes.length > 0) { this.applied.set(false); - const previewSource = this._buildMermaidFromChanges(result.changes); - const parsedChanges = result.changes.map(c => this._parseChange(c)); - this.messages.update(msgs => { - const next: ChatMessage[] = [...msgs, { - role: 'ai', - text: `I've generated ${result.changes.length} change(s) for your database. Review them below and approve or reject.`, - changes: result.changes, - parsedChanges, - batchId: result.batchId, - }]; + let previewSource = ''; + try { + const preview = await this._tableSchema.previewDiagram( + this.connectionID, + result.changes.map((c) => c.forwardSql), + ); + previewSource = preview.diagram ?? ''; + } catch { + // preview is supplementary — don't block the change list on a diagram failure + } + const parsedChanges = result.changes.map((c) => this._parseChange(c)); + this.messages.update((msgs) => { + const next: ChatMessage[] = [ + ...msgs, + { + role: 'ai', + text: `I've generated ${result.changes.length} change(s) for your database. Review them below and approve or reject.`, + changes: result.changes, + parsedChanges, + batchId: result.batchId, + }, + ]; if (previewSource) { next.push({ role: 'diagram', @@ -152,17 +162,23 @@ export class EditDatabaseSchemaComponent implements OnInit { return next; }); } else { - this.messages.update(msgs => [...msgs, { - role: 'ai', - text: 'I could not generate any schema changes for that prompt. Could you describe your database in more detail?', - }]); + this.messages.update((msgs) => [ + ...msgs, + { + role: 'ai', + text: 'I could not generate any schema changes for that prompt. Could you describe your database in more detail?', + }, + ]); } } catch (err: unknown) { const error = err as { error?: { message?: string }; message?: string }; - this.messages.update(msgs => [...msgs, { - role: 'error', - text: error?.error?.message || error?.message || 'Failed to generate schema changes.', - }]); + this.messages.update((msgs) => [ + ...msgs, + { + role: 'error', + text: error?.error?.message || error?.message || 'Failed to generate schema changes.', + }, + ]); } finally { this.submitting.set(false); } @@ -177,29 +193,37 @@ export class EditDatabaseSchemaComponent implements OnInit { try { const result = await this._tableSchema.approveBatch(batch.batchId, true); if (result) { - const failed = result.changes.filter(c => c.status === 'failed'); + const failed = result.changes.filter((c) => c.status === 'failed'); if (failed.length > 0) { - this.messages.update(msgs => [...msgs, { - role: 'error', - text: `${failed.length} change(s) failed: ${failed.map(c => c.executionError).join('; ')}`, - }]); + this.messages.update((msgs) => [ + ...msgs, + { + role: 'error', + text: `${failed.length} change(s) failed: ${failed.map((c) => c.executionError).join('; ')}`, + }, + ]); } else { this.applied.set(true); - this.messages.update(msgs => msgs.map(m => - m === batch ? { ...m, batchId: undefined } : m - ).concat({ - role: 'ai', - text: 'All changes applied successfully! Your tables have been created.', - })); + this.messages.update((msgs) => + msgs + .map((m) => (m === batch ? { ...m, batchId: undefined } : m)) + .concat({ + role: 'ai', + text: 'All changes applied successfully! Your tables have been created.', + }), + ); this._loadDiagram('Updated Database Structure'); } } } catch (err: unknown) { const error = err as { error?: { message?: string }; message?: string }; - this.messages.update(msgs => [...msgs, { - role: 'error', - text: error?.error?.message || error?.message || 'Failed to apply schema changes.', - }]); + this.messages.update((msgs) => [ + ...msgs, + { + role: 'error', + text: error?.error?.message || error?.message || 'Failed to apply schema changes.', + }, + ]); } finally { this.applying.set(false); } @@ -210,13 +234,15 @@ export class EditDatabaseSchemaComponent implements OnInit { if (!batch?.batchId) return; await this._tableSchema.rejectBatch(batch.batchId); - this.messages.update(msgs => msgs - .filter(m => !(m.role === 'diagram' && m.text === 'Schema Preview')) - .map(m => m === batch ? { ...m, batchId: undefined } : m) - .concat({ - role: 'ai', - text: 'Changes rejected. Feel free to describe what you need differently.', - })); + this.messages.update((msgs) => + msgs + .filter((m) => !(m.role === 'diagram' && m.text === 'Schema Preview')) + .map((m) => (m === batch ? { ...m, batchId: undefined } : m)) + .concat({ + role: 'ai', + text: 'Changes rejected. Feel free to describe what you need differently.', + }), + ); } onOpenTables() { @@ -236,11 +262,14 @@ export class EditDatabaseSchemaComponent implements OnInit { const diagram = await this._tableSchema.fetchDiagram(this.connectionID); const source = diagram?.diagram ?? ''; if (this._mermaidHasEntities(source)) { - this.messages.update(msgs => [...msgs, { - role: 'diagram' as const, - text: label, - diagramSource: '```mermaid\n' + source + '\n```', - }]); + this.messages.update((msgs) => [ + ...msgs, + { + role: 'diagram' as const, + text: label, + diagramSource: '```mermaid\n' + source + '\n```', + }, + ]); } } catch { // Diagram is supplementary - don't show error if it fails @@ -257,107 +286,6 @@ export class EditDatabaseSchemaComponent implements OnInit { return false; } - private _buildMermaidFromChanges(changes: SchemaChangeResponse[]): string { - const parser = new Parser(); - const tables: { name: string; columns: { type: string; name: string; pk: boolean; fk: boolean }[] }[] = []; - const relationKeys = new Set(); - const relations: { from: string; to: string }[] = []; - - for (const change of changes) { - const sql = change.forwardSql?.trim(); - if (!sql) continue; - - let parsed: unknown; - try { - parsed = parser.astify(sql, { database: 'PostgresQL' }); - } catch { - try { - parsed = parser.astify(sql, { database: 'MySQL' }); - } catch { - continue; - } - } - - const nodes = Array.isArray(parsed) ? parsed : [parsed]; - for (const node of nodes) { - const ast = node as Record | null; - if (!ast || ast['type'] !== 'create' || ast['keyword'] !== 'table') continue; - - const tableName = this._extractTableName(ast['table']); - if (!tableName) continue; - - const columns: { type: string; name: string; pk: boolean; fk: boolean }[] = []; - const colIndex = new Map(); - const defs = (ast['create_definitions'] as unknown[]) ?? []; - - for (const rawDef of defs) { - const def = rawDef as Record; - if (def['resource'] === 'column') { - const colName = this._extractColumnName(def['column']); - if (!colName) continue; - const dataType = (def['definition'] as { dataType?: string } | undefined)?.dataType ?? ''; - const primary = def['primary'] as string | undefined; - const pk = primary === 'primary key' || primary === 'key'; - const ref = this._extractReferenceTable(def['reference_definition']); - const fk = !!ref; - if (ref) this._pushRelation(relations, relationKeys, tableName, ref); - colIndex.set(colName, columns.length); - columns.push({ name: colName, type: dataType.toLowerCase(), pk, fk }); - } else if (def['resource'] === 'constraint') { - const ctype = (def['constraint_type'] as string | undefined)?.toLowerCase(); - const colRefs = (def['definition'] as unknown[]) ?? []; - if (ctype === 'primary key') { - for (const c of colRefs) { - const name = this._extractColumnName(c); - if (!name) continue; - const i = colIndex.get(name); - if (i !== undefined) columns[i].pk = true; - } - } else if (ctype === 'foreign key') { - const ref = this._extractReferenceTable(def['reference_definition']); - if (!ref) continue; - for (const c of colRefs) { - const name = this._extractColumnName(c); - if (name) { - const i = colIndex.get(name); - if (i !== undefined) columns[i].fk = true; - } - } - this._pushRelation(relations, relationKeys, tableName, ref); - } - } - } - - if (columns.length > 0) tables.push({ name: tableName, columns }); - } - } - - if (tables.length === 0) return ''; - - let out = 'erDiagram\n'; - for (const rel of relations) { - if (tables.some(t => t.name === rel.to)) { - out += ` ${rel.to} ||--o{ ${rel.from} : has\n`; - } - } - for (const t of tables) { - out += ` ${t.name} {\n`; - for (const col of t.columns) { - const tag = col.pk ? ' PK' : col.fk ? ' FK' : ''; - out += ` ${col.type || 'string'} ${col.name}${tag}\n`; - } - out += ` }\n`; - } - return out; - } - - private _extractTableName(value: unknown): string | null { - if (!value) return null; - const first = Array.isArray(value) ? value[0] : value; - const name = (first as { table?: unknown })?.table; - return typeof name === 'string' ? name : null; - } - private _extractColumnName(value: unknown): string | null { if (!value) return null; const inner = (value as { column?: unknown }).column ?? value; @@ -376,18 +304,6 @@ export class EditDatabaseSchemaComponent implements OnInit { return typeof name === 'string' ? name : null; } - private _pushRelation( - relations: { from: string; to: string }[], - seen: Set, - from: string, - to: string, - ): void { - const key = `${from}|${to}`; - if (seen.has(key)) return; - seen.add(key); - relations.push({ from, to }); - } - private _changeMeta(changeType: string): { action: string; icon: string; tone: 'add' | 'edit' | 'remove' } { const map: Record = { CREATE_TABLE: { action: 'Create table', icon: 'add_box', tone: 'add' }, @@ -415,11 +331,16 @@ export class EditDatabaseSchemaComponent implements OnInit { ROLLBACK: { action: 'Rollback', icon: 'undo', tone: 'edit' }, }; const upper = (changeType ?? '').toUpperCase(); - return map[upper] ?? { - action: upper.replace(/_/g, ' ').toLowerCase().replace(/\b\w/, c => c.toUpperCase()), - icon: 'code', - tone: 'edit', - }; + return ( + map[upper] ?? { + action: upper + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/, (c) => c.toUpperCase()), + icon: 'code', + tone: 'edit', + } + ); } private _parseChange(change: SchemaChangeResponse): ParsedChange { @@ -515,7 +436,7 @@ export class EditDatabaseSchemaComponent implements OnInit { const v = (inner as { value?: unknown }).value; if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v); if ((inner as { type?: string }).type === 'function') { - const name = ((inner as { name?: { name?: { value?: string }[] } }).name?.name?.[0]?.value) ?? ''; + const name = (inner as { name?: { name?: { value?: string }[] } }).name?.name?.[0]?.value ?? ''; return name ? `${name.toUpperCase()}()` : undefined; } return undefined; diff --git a/frontend/src/app/services/table-schema.service.ts b/frontend/src/app/services/table-schema.service.ts index 9ca9e9634..f03582138 100644 --- a/frontend/src/app/services/table-schema.service.ts +++ b/frontend/src/app/services/table-schema.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; -import { ApiService } from './api.service'; import { environment } from '../../environments/environment'; +import { ApiService } from './api.service'; export interface ConnectionDiagramResponse { connectionId: string; @@ -10,6 +10,30 @@ export interface ConnectionDiagramResponse { generatedAt: string; } +export interface ConnectionDiagramPreviewStatementResult { + sql: string; + status: 'applied' | 'skipped' | 'error'; + message?: string; +} + +export interface ConnectionDiagramPreviewDiff { + addedTables: string[]; + droppedTables: string[]; + addedColumns: Record; + droppedColumns: Record; + addedForeignKeys: Record; + statementResults: ConnectionDiagramPreviewStatementResult[]; +} + +export interface ConnectionDiagramPreviewResponse { + connectionId: string; + databaseType: string; + diagram: string; + description: string; + diff: ConnectionDiagramPreviewDiff; + generatedAt: string; +} + export interface SchemaChangeResponse { id: string; connectionId: string; @@ -44,8 +68,15 @@ export interface SchemaChangeBatchResponse { export class TableSchemaService { private _api = inject(ApiService); - async generateSchemaChange(connectionId: string, userPrompt: string, threadId?: string): Promise { - return this._fetchOrThrow(`/table-schema/${connectionId}/generate`, { userPrompt, threadId }); + async generateSchemaChange( + connectionId: string, + userPrompt: string, + threadId?: string, + ): Promise { + return this._fetchOrThrow(`/table-schema/${connectionId}/generate`, { + userPrompt, + threadId, + }); } async approveBatch(batchId: string, confirmedDestructive?: boolean): Promise { @@ -62,6 +93,12 @@ export class TableSchemaService { return this._api.get(`/connection/diagram/${connectionId}`); } + async previewDiagram(connectionId: string, sqlCommands: string[]): Promise { + return this._fetchOrThrow(`/connection/diagram/${connectionId}/preview`, { + sqlCommands, + }); + } + private async _fetchOrThrow(url: string, body: unknown): Promise { const fullUrl = `${environment.apiRoot || '/api'}${url}`; const response = await fetch(fullUrl, { diff --git a/package.json b/package.json index 7d4cb7218..368a53caf 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,14 @@ "lodash@>=4.0.0 <=4.17.23": ">=4.18.0", "lodash@<=4.17.23": ">=4.18.0", "@nestjs/core@<=11.1.17": ">=11.1.18", - "uuid@<14.0.0": ">=14.0.0" + "uuid@<14.0.0": ">=14.0.0", + "fast-uri@<=3.1.0": ">=3.1.1", + "fast-uri@<=3.1.1": ">=3.1.2", + "langsmith@<0.6.0": ">=0.6.0", + "ip-address@<=10.1.0": ">=10.1.1", + "fast-xml-builder@<=1.1.6": ">=1.1.7", + "fast-xml-builder@=1.1.5": ">=1.1.6", + "brace-expansion@>=5.0.0 <5.0.6": ">=5.0.6" }, "packageExtensions": { "ibm_db": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9feb14a3e..e46ada084 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,13 @@ overrides: lodash@<=4.17.23: '>=4.18.0' '@nestjs/core@<=11.1.17': '>=11.1.18' uuid@<14.0.0: '>=14.0.0' + fast-uri@<=3.1.0: '>=3.1.1' + fast-uri@<=3.1.1: '>=3.1.2' + langsmith@<0.6.0: '>=0.6.0' + ip-address@<=10.1.0: '>=10.1.1' + fast-xml-builder@<=1.1.6: '>=1.1.7' + fast-xml-builder@=1.1.5: '>=1.1.6' + brace-expansion@>=5.0.0 <5.0.6: '>=5.0.6' packageExtensionsChecksum: sha256-qM/gPCDCAwbN43hScuRM/oThrQiWcAMxRCqJ3FQdbM0= @@ -2795,8 +2802,8 @@ packages: brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -3440,15 +3447,12 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fast-xml-builder@1.1.5: - resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} - fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -3769,8 +3773,8 @@ packages: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} ip-range-check@0.2.0: @@ -4017,8 +4021,8 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - langsmith@0.5.20: - resolution: {integrity: sha512-ULhLM8RswvQDXufLtNtvclHrWCBx8Cb5UPI6lAZC+8Dq59iHsVPz/3Ac9khWNm1VIvChRsuykixD/WrmzuuA3Q==} + langsmith@0.7.1: + resolution: {integrity: sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' @@ -7055,7 +7059,7 @@ snapshots: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.5.20(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + langsmith: 0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.4.3 @@ -8532,7 +8536,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -8746,7 +8750,7 @@ snapshots: balanced-match: 1.0.2 optional: true - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -9368,16 +9372,12 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fast-wrap-ansi@0.2.0: dependencies: fast-string-width: 3.0.2 - fast-xml-builder@1.1.5: - dependencies: - path-expression-matcher: 1.5.0 - fast-xml-builder@1.2.0: dependencies: path-expression-matcher: 1.5.0 @@ -9386,7 +9386,7 @@ snapshots: fast-xml-parser@5.7.1: dependencies: '@nodable/entities': 2.1.0 - fast-xml-builder: 1.1.5 + fast-xml-builder: 1.2.0 path-expression-matcher: 1.5.0 strnum: 2.2.3 @@ -9748,7 +9748,7 @@ snapshots: interpret@2.2.0: {} - ip-address@10.1.0: + ip-address@10.2.0: optional: true ip-range-check@0.2.0: @@ -9972,10 +9972,9 @@ snapshots: kuler@2.0.0: {} - langsmith@0.5.20(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): + langsmith@0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): dependencies: p-queue: 6.6.2 - uuid: 14.0.0 optionalDependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1) @@ -10160,7 +10159,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -10964,7 +10963,7 @@ snapshots: socks@2.8.7: dependencies: - ip-address: 10.1.0 + ip-address: 10.2.0 smart-buffer: 4.2.0 optional: true