From 402176d46b0c79ab5d4bf106f969c3f577ee84bd Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 17 Jul 2024 11:23:13 +0800 Subject: [PATCH 01/53] feat: remove ai package --- .../home/insight/insight.component.ts | 199 ++++--- .../register-form/register-form.component.ts | 4 +- .../access-control.component.ts | 82 +-- .../role/cube/cube.component.ts | 14 +- .../virtual-cube/virtual-cube.component.ts | 52 +- libs/story-angular/story/copilot/index.ts | 1 - .../story/copilot/math.command.ts | 94 ---- .../story/copilot/measure.command.ts | 48 +- .../story/copilot/page.command.ts | 116 ++-- .../story/copilot/style.command.ts | 46 +- .../copilot-angular/src/lib/hooks/index.ts | 1 - .../hooks/inject-make-copilot-actionable.ts | 47 +- .../src/lib/services/context.service.ts | 93 ++-- packages/copilot/src/index.ts | 2 - packages/copilot/src/lib/shared/functions.ts | 191 ++++--- .../src/lib/shared/process-chat-stream.ts | 509 +++++++++--------- .../src/lib/types/annotated-function.ts | 74 +-- packages/copilot/src/lib/types/index.ts | 1 - 18 files changed, 734 insertions(+), 840 deletions(-) delete mode 100644 libs/story-angular/story/copilot/math.command.ts diff --git a/apps/cloud/src/app/features/home/insight/insight.component.ts b/apps/cloud/src/app/features/home/insight/insight.component.ts index 174b44ac8..e0f9b4a9e 100644 --- a/apps/cloud/src/app/features/home/insight/insight.component.ts +++ b/apps/cloud/src/app/features/home/insight/insight.component.ts @@ -11,7 +11,7 @@ import { MatDialog } from '@angular/material/dialog' import { Title } from '@angular/platform-browser' import { Router, RouterModule } from '@angular/router' import { NgmSemanticModel } from '@metad/cloud/state' -import { FunctionCallHandlerOptions, zodToAnnotations } from '@metad/copilot' +import { zodToAnnotations } from '@metad/copilot' import { calcEntityTypePrompt, makeChartDimensionSchema, @@ -26,7 +26,6 @@ import { NgmCopilotEnableComponent, NgmCopilotInputComponent, injectCopilotCommand, - injectMakeCopilotActionable } from '@metad/copilot-angular' import { AppearanceDirective, ButtonGroupDirective, DensityDirective } from '@metad/ocap-angular/core' import { NgmEntityPropertyComponent } from '@metad/ocap-angular/entity' @@ -197,84 +196,84 @@ ${calcEntityTypePrompt(entityType)} ` }, actions: [ - injectMakeCopilotActionable({ - name: 'new_chart', - description: 'New a chart', - argumentAnnotations: [ - { - name: 'chart', - description: 'Chart configuration', - type: 'object', - properties: zodToAnnotations(makeChartSchema()), - required: true - }, - { - name: 'dimension', - description: 'dimension configuration for chart', - type: 'object', - properties: zodToAnnotations(makeChartDimensionSchema()), - required: true - }, - { - name: 'measure', - description: 'measure configuration for chart', - type: 'object', - properties: zodToAnnotations(makeChartMeasureSchema()), - required: true - } - ], - implementation: async (chart: any, dimension, measure, options: FunctionCallHandlerOptions) => { - this.#logger.debug('New chart by copilot command with:', chart, dimension, measure, options) - const userMessage = options.messages.reverse().find((item) => item.role === 'user') - const dataSourceName = this.insightService.dataSourceName() - const cubes = this.insightService.allCubes() - - try { - chart.cube ??= this.entityType().name - const { chartAnnotation, slicers, limit, chartOptions } = transformCopilotChart( - { - ...chart, - dimension, - measure - }, - this.entityType() - ) - const answerMessage: Partial = { - key: options.conversationId, - title: userMessage?.content, - message: JSON.stringify(chart, null, 2), - dataSettings: { - dataSource: dataSourceName, - entitySet: chart.cube, - chartAnnotation, - presentationVariant: { - maxItems: limit, - groupBy: getEntityDimensions(this.entityType()).map((property) => ({ - dimension: property.name, - hierarchy: property.defaultHierarchy, - level: null - })) - } - } as DataSettings, - slicers, - chartOptions, - isCube: cubes.find((item) => item.name === chart.cube), - answering: false, - expanded: true - } - - this.#logger.debug('New chart by copilot command is:', answerMessage) - this.updateAnswer(answerMessage) - return `✅` - } catch (err: any) { - return { - id: nanoid(), - role: 'function', - content: `Error: ${err.message}` - } - } - } - }) + // injectMakeCopilotActionable({ + // name: 'new_chart', + // description: 'New a chart', + // argumentAnnotations: [ + // { + // name: 'chart', + // description: 'Chart configuration', + // type: 'object', + // properties: zodToAnnotations(makeChartSchema()), + // required: true + // }, + // { + // name: 'dimension', + // description: 'dimension configuration for chart', + // type: 'object', + // properties: zodToAnnotations(makeChartDimensionSchema()), + // required: true + // }, + // { + // name: 'measure', + // description: 'measure configuration for chart', + // type: 'object', + // properties: zodToAnnotations(makeChartMeasureSchema()), + // required: true + // } + // ], + // implementation: async (chart: any, dimension, measure, options: FunctionCallHandlerOptions) => { + // this.#logger.debug('New chart by copilot command with:', chart, dimension, measure, options) + // const userMessage = options.messages.reverse().find((item) => item.role === 'user') + // const dataSourceName = this.insightService.dataSourceName() + // const cubes = this.insightService.allCubes() + + // try { + // chart.cube ??= this.entityType().name + // const { chartAnnotation, slicers, limit, chartOptions } = transformCopilotChart( + // { + // ...chart, + // dimension, + // measure + // }, + // this.entityType() + // ) + // const answerMessage: Partial = { + // key: options.conversationId, + // title: userMessage?.content, + // message: JSON.stringify(chart, null, 2), + // dataSettings: { + // dataSource: dataSourceName, + // entitySet: chart.cube, + // chartAnnotation, + // presentationVariant: { + // maxItems: limit, + // groupBy: getEntityDimensions(this.entityType()).map((property) => ({ + // dimension: property.name, + // hierarchy: property.defaultHierarchy, + // level: null + // })) + // } + // } as DataSettings, + // slicers, + // chartOptions, + // isCube: cubes.find((item) => item.name === chart.cube), + // answering: false, + // expanded: true + // } + + // this.#logger.debug('New chart by copilot command is:', answerMessage) + // this.updateAnswer(answerMessage) + // return `✅` + // } catch (err: any) { + // return { + // id: nanoid(), + // role: 'function', + // content: `Error: ${err.message}` + // } + // } + // } + // }) ] }) @@ -305,26 +304,26 @@ ${calcEntityTypePrompt(entityType)} ` }, actions: [ - injectMakeCopilotActionable({ - name: 'suggest', - description: 'Suggests prompts for cube', - argumentAnnotations: [ - { - name: 'param', - description: 'Prompt', - type: 'object', - required: true, - properties: zodToProperties(SuggestsSchema) - } - ], - implementation: async (param: { suggests: string[] }, options: FunctionCallHandlerOptions) => { - this.#logger.debug('Suggest prompts by copilot command with:', param, options) - if (param?.suggests) { - this.insightService.updateSuggests(param.suggests) - } - return `✅` - } - }) + // injectMakeCopilotActionable({ + // name: 'suggest', + // description: 'Suggests prompts for cube', + // argumentAnnotations: [ + // { + // name: 'param', + // description: 'Prompt', + // type: 'object', + // required: true, + // properties: zodToProperties(SuggestsSchema) + // } + // ], + // implementation: async (param: { suggests: string[] }, options: FunctionCallHandlerOptions) => { + // this.#logger.debug('Suggest prompts by copilot command with:', param, options) + // if (param?.suggests) { + // this.insightService.updateSuggests(param.suggests) + // } + // return `✅` + // } + // }) ] }) diff --git a/apps/cloud/src/app/features/project/indicators/register-form/register-form.component.ts b/apps/cloud/src/app/features/project/indicators/register-form/register-form.component.ts index cc8b3fa44..37688c1b3 100644 --- a/apps/cloud/src/app/features/project/indicators/register-form/register-form.component.ts +++ b/apps/cloud/src/app/features/project/indicators/register-form/register-form.component.ts @@ -13,8 +13,8 @@ import { import { MatDialog } from '@angular/material/dialog' import { MatFormFieldAppearance } from '@angular/material/form-field' import { NgmSemanticModel } from '@metad/cloud/state' -import { CommandDialogComponent, injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' -import { IsNilPipe, calcEntityTypePrompt, nonBlank, nonNullable } from '@metad/core' +import { CommandDialogComponent } from '@metad/copilot-angular' +import { IsNilPipe, nonBlank, nonNullable } from '@metad/core' import { NgmHierarchySelectComponent, NgmMatSelectComponent, NgmTreeSelectComponent } from '@metad/ocap-angular/common' import { ISelectOption, NgmDSCoreService } from '@metad/ocap-angular/core' import { NgmCalculatedMeasureComponent } from '@metad/ocap-angular/entity' diff --git a/apps/cloud/src/app/features/semantic-model/model/access-control/access-control.component.ts b/apps/cloud/src/app/features/semantic-model/model/access-control/access-control.component.ts index 1fffce4ef..d271a286f 100644 --- a/apps/cloud/src/app/features/semantic-model/model/access-control/access-control.component.ts +++ b/apps/cloud/src/app/features/semantic-model/model/access-control/access-control.component.ts @@ -19,7 +19,7 @@ import { ActivatedRoute, Router } from '@angular/router' import { IModelRole } from '@metad/contracts' import { calcEntityTypePrompt } from '@metad/core' import { NgmDisplayBehaviourComponent, NgmSearchComponent } from '@metad/ocap-angular/common' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { ButtonGroupDirective, ISelectOption } from '@metad/ocap-angular/core' import { cloneDeep } from '@metad/ocap-core' import { uuid } from 'apps/cloud/src/app/@core' @@ -106,48 +106,48 @@ export class AccessControlComponent extends TranslationBaseComponent { }), systemPrompt: async () => `Create or edit a role. 如何未提供 cube 信息,请先选择一个 cube`, actions: [ - injectMakeCopilotActionable({ - name: 'select_cube', - description: 'Select a cube', - argumentAnnotations: [], - implementation: async () => { - const result = await firstValueFrom( - this.#dialog.open(CubeSelectorComponent, { data: this.cubes() }).afterClosed() - ) - if (result) { - const entityType = await firstValueFrom(this.#modelService.selectEntityType(result[0])) - return { - id: nanoid(), - name: 'select_cube', - role: 'function', - content: `${calcEntityTypePrompt(entityType)}` - } - } - } - }), - injectMakeCopilotActionable({ - name: 'new-role', - description: 'Create a new role', - argumentAnnotations: [ - { - name: 'role', - type: 'object', - description: 'Role defination', - properties: zodToAnnotations(RoleSchema), - required: true - } - ], - implementation: async (role: any) => { - role.key = nanoid() - this.#logger.debug(`The new role in function call is:`, role) - this.#accessControlState.addRole(role) + // injectMakeCopilotActionable({ + // name: 'select_cube', + // description: 'Select a cube', + // argumentAnnotations: [], + // implementation: async () => { + // const result = await firstValueFrom( + // this.#dialog.open(CubeSelectorComponent, { data: this.cubes() }).afterClosed() + // ) + // if (result) { + // const entityType = await firstValueFrom(this.#modelService.selectEntityType(result[0])) + // return { + // id: nanoid(), + // name: 'select_cube', + // role: 'function', + // content: `${calcEntityTypePrompt(entityType)}` + // } + // } + // } + // }), + // injectMakeCopilotActionable({ + // name: 'new-role', + // description: 'Create a new role', + // argumentAnnotations: [ + // { + // name: 'role', + // type: 'object', + // description: 'Role defination', + // properties: zodToAnnotations(RoleSchema), + // required: true + // } + // ], + // implementation: async (role: any) => { + // role.key = nanoid() + // this.#logger.debug(`The new role in function call is:`, role) + // this.#accessControlState.addRole(role) - // Navigate to the new role - this.#router.navigate([role.key], { relativeTo: this.#route }) + // // Navigate to the new role + // this.#router.navigate([role.key], { relativeTo: this.#route }) - return `创建成功` - } - }) + // return `创建成功` + // } + // }) ] }) diff --git a/apps/cloud/src/app/features/semantic-model/model/access-control/role/cube/cube.component.ts b/apps/cloud/src/app/features/semantic-model/model/access-control/role/cube/cube.component.ts index 2770fc1da..93a2cf97b 100644 --- a/apps/cloud/src/app/features/semantic-model/model/access-control/role/cube/cube.component.ts +++ b/apps/cloud/src/app/features/semantic-model/model/access-control/role/cube/cube.component.ts @@ -7,7 +7,7 @@ import { MatSlideToggleChange } from '@angular/material/slide-toggle' import { ActivatedRoute, Router } from '@angular/router' import { MDX } from '@metad/contracts' import { nonBlank } from '@metad/core' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { EntitySchemaType } from '@metad/ocap-angular/entity' import { AggregationRole, C_MEASURES, EntityType, getEntityHierarchy } from '@metad/ocap-core' import { TranslateService } from '@ngx-translate/core' @@ -79,12 +79,12 @@ export class CubeComponent { }), systemPrompt: async () => `Create or edit a role. 如何未提供 cube 信息,请先选择一个 cube`, actions: [ - injectMakeCopilotActionable({ - name: 'select_cube', - description: 'Select a cube', - argumentAnnotations: [], - implementation: async () => {} - }) + // injectMakeCopilotActionable({ + // name: 'select_cube', + // description: 'Select a cube', + // argumentAnnotations: [], + // implementation: async () => {} + // }) ] }) /** diff --git a/apps/cloud/src/app/features/semantic-model/model/virtual-cube/virtual-cube.component.ts b/apps/cloud/src/app/features/semantic-model/model/virtual-cube/virtual-cube.component.ts index 2e80c4efa..e09b0b84f 100644 --- a/apps/cloud/src/app/features/semantic-model/model/virtual-cube/virtual-cube.component.ts +++ b/apps/cloud/src/app/features/semantic-model/model/virtual-cube/virtual-cube.component.ts @@ -11,7 +11,7 @@ import { NgmDialogComponent } from '@metad/components/dialog' import { MDX } from '@metad/contracts' import { calcEntityTypePrompt, nonBlank } from '@metad/core' import { NgmCommonModule, ResizerModule } from '@metad/ocap-angular/common' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { NgmDSCoreService, OcapCoreModule } from '@metad/ocap-angular/core' import { EntityCapacity, NgmCalculatedMeasureComponent, NgmEntitySchemaComponent } from '@metad/ocap-angular/entity' import { AggregationRole, C_MEASURES, Syntax } from '@metad/ocap-core' @@ -153,31 +153,31 @@ ${calcEntityTypePrompt(this.entityType())} return prompt }, actions: [ - injectMakeCopilotActionable({ - name: 'new_or_edit_calculated_member', - description: 'Create a new formula for the calculated measure', - argumentAnnotations: [ - { - name: 'formula', - type: 'string', - description: 'provide the new formula', - required: true - }, - { - name: 'unit', - type: 'string', - description: 'unit of the formula', - required: true - } - ], - implementation: async (formula: string, unit: string) => { - this.#logger.debug(`Copilot command 'formula' params: formula is`, formula, `unit is`, unit) - this.calcMemberFormGroup.patchValue({ formula, unit }) - this.showCalculatedMember.set(true) - - return `✅` - } - }) + // injectMakeCopilotActionable({ + // name: 'new_or_edit_calculated_member', + // description: 'Create a new formula for the calculated measure', + // argumentAnnotations: [ + // { + // name: 'formula', + // type: 'string', + // description: 'provide the new formula', + // required: true + // }, + // { + // name: 'unit', + // type: 'string', + // description: 'unit of the formula', + // required: true + // } + // ], + // implementation: async (formula: string, unit: string) => { + // this.#logger.debug(`Copilot command 'formula' params: formula is`, formula, `unit is`, unit) + // this.calcMemberFormGroup.patchValue({ formula, unit }) + // this.showCalculatedMember.set(true) + + // return `✅` + // } + // }) ] }) diff --git a/libs/story-angular/story/copilot/index.ts b/libs/story-angular/story/copilot/index.ts index a18f7d177..836043ebd 100644 --- a/libs/story-angular/story/copilot/index.ts +++ b/libs/story-angular/story/copilot/index.ts @@ -1,4 +1,3 @@ -export * from './math.command' export * from './measure.command' export * from './page.command' export * from './schema/' diff --git a/libs/story-angular/story/copilot/math.command.ts b/libs/story-angular/story/copilot/math.command.ts deleted file mode 100644 index 92200e16e..000000000 --- a/libs/story-angular/story/copilot/math.command.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { inject } from '@angular/core' -import { zodToProperties } from '@metad/core' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' -import { NxStoryService } from '@metad/story/core' -import { NGXLogger } from 'ngx-logger' -import { ChartWidgetSchema } from './schema' -import { DynamicStructuredTool } from "@langchain/core/tools"; -import { z } from "zod"; -import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; - - -/** - */ -export function injectMathCommand(storyService: NxStoryService) { - const logger = inject(NGXLogger) - - const widget = storyService.currentWidget() - const page = storyService.currentStoryPoint() - - logger.debug(`Original chart widget is`, widget, page) - - const addTool = new DynamicStructuredTool({ - name: "add", - description: "Add two integers together.", - schema: z.object({ - firstInt: z.number(), - secondInt: z.number(), - }), - func: async ({ firstInt, secondInt }) => { - return (firstInt + secondInt).toString(); - }, - }); - - const multiplyTool = new DynamicStructuredTool({ - name: "multiply", - description: "Multiply two integers together.", - schema: z.object({ - firstInt: z.number(), - secondInt: z.number(), - }), - func: async ({ firstInt, secondInt }) => { - return (firstInt * secondInt).toString(); - }, - }); - - const exponentiateTool = new DynamicStructuredTool({ - name: "exponentiate", - description: "Exponentiate the base to the exponent power.", - schema: z.object({ - base: z.number(), - exponent: z.number(), - }), - func: async ({ base, exponent }) => { - return (base ** exponent).toString(); - }, - }); - - const tools = [addTool, multiplyTool, exponentiateTool]; - - return injectCopilotCommand({ - name: 'math', - description: 'Describe what you want to calculate', - actions: [ - injectMakeCopilotActionable({ - name: 'modify_widget', - description: 'Modify widget component settings', - argumentAnnotations: [ - { - name: 'widget', - type: 'object', - description: 'Widget settings', - properties: zodToProperties(ChartWidgetSchema), - required: true - } - ], - implementation: async (widget) => { - logger.debug(`Function calling 'modify_widget', params is:`, widget) - - return `✅` - } - }) - ], - tools, - prompt: ChatPromptTemplate.fromMessages([ - ["system", "You are a helpful assistant"], - new MessagesPlaceholder({ - variableName:"chat_history", - optional: true - }), - ["user", "{input}"], - new MessagesPlaceholder("agent_scratchpad"), - ]) - }) -} diff --git a/libs/story-angular/story/copilot/measure.command.ts b/libs/story-angular/story/copilot/measure.command.ts index 371100d20..1aeab5105 100644 --- a/libs/story-angular/story/copilot/measure.command.ts +++ b/libs/story-angular/story/copilot/measure.command.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core' import { zodToProperties } from '@metad/core' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { NxStoryService } from '@metad/story/core' import { NGXLogger } from 'ngx-logger' import { CalculationMeasureSchema } from './schema/story.schema' @@ -22,32 +22,32 @@ export function injectCalclatedMeasureCommand( return `Create a calculatation measure` }, actions: [ - injectMakeCopilotActionable({ - name: 'create_calculation_measure', - description: '', - argumentAnnotations: [ - { - name: 'calculatedMeasure', - type: 'object', - description: 'The calculated measure', - properties: zodToProperties(CalculationMeasureSchema), - required: true - } - ], - implementation: async (calculation) => { - logger.debug(`Function calling 'create_calculation_measure', params is:`, calculation) + // injectMakeCopilotActionable({ + // name: 'create_calculation_measure', + // description: '', + // argumentAnnotations: [ + // { + // name: 'calculatedMeasure', + // type: 'object', + // description: 'The calculated measure', + // properties: zodToProperties(CalculationMeasureSchema), + // required: true + // } + // ], + // implementation: async (calculation) => { + // logger.debug(`Function calling 'create_calculation_measure', params is:`, calculation) - storyService.addCalculationMeasure({ dataSettings, calculation }) + // storyService.addCalculationMeasure({ dataSettings, calculation }) - if (callback) { - await callback(calculation) - } + // if (callback) { + // await callback(calculation) + // } - return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { - Default: 'Instruction Execution Complete' - })}` - } - }) + // return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { + // Default: 'Instruction Execution Complete' + // })}` + // } + // }) ] }) } diff --git a/libs/story-angular/story/copilot/page.command.ts b/libs/story-angular/story/copilot/page.command.ts index 3c15ab6c4..342ccbbd1 100644 --- a/libs/story-angular/story/copilot/page.command.ts +++ b/libs/story-angular/story/copilot/page.command.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core' import { calcEntityTypePrompt, zodToProperties } from '@metad/core' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { EntityType } from '@metad/ocap-core' import { NxStoryService, StoryPointType } from '@metad/story/core' import { nanoid } from 'nanoid' @@ -37,63 +37,63 @@ ${calcEntityTypePrompt(defaultCube)} return prompt }, actions: [ - injectMakeCopilotActionable({ - name: 'pick_default_cube', - description: 'Pick a default cube', - argumentAnnotations: [], - implementation: async () => { - const result = await storyService.openDefultDataSettings() - logger.debug(`Pick the default cube is:`, result) - if (result?.dataSource && result?.entities[0]) { - dataSourceName = result.dataSource - const entityType = await firstValueFrom(storyService.selectEntityType({dataSource: result.dataSource, entitySet: result.entities[0]})) - defaultCube = entityType - } - return { - id: nanoid(), - role: 'function', - content: `The cube is: -\`\`\` -${calcEntityTypePrompt(defaultCube)} -\`\`\` -` - } - } - }), - injectMakeCopilotActionable({ - name: 'new_story_page', - description: '', - argumentAnnotations: [ - { - name: 'page', - type: 'object', - description: 'Page config', - properties: zodToProperties(StoryPageSchema), - required: true - }, - { - name: 'widgets', - type: 'array', - description: 'Widgets in page config', - items: { - type: 'object', - properties: zodToProperties(StoryWidgetSchema) - }, - required: true - } - ], - implementation: async (page, widgets) => { - logger.debug(`Function calling 'new_story_page', params is:`, page, widgets) - storyService.newStoryPage({ - ...page, - type: StoryPointType.Canvas, - // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) - }) - return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { - Default: 'Instruction Execution Complete' - })}` - } - }) +// injectMakeCopilotActionable({ +// name: 'pick_default_cube', +// description: 'Pick a default cube', +// argumentAnnotations: [], +// implementation: async () => { +// const result = await storyService.openDefultDataSettings() +// logger.debug(`Pick the default cube is:`, result) +// if (result?.dataSource && result?.entities[0]) { +// dataSourceName = result.dataSource +// const entityType = await firstValueFrom(storyService.selectEntityType({dataSource: result.dataSource, entitySet: result.entities[0]})) +// defaultCube = entityType +// } +// return { +// id: nanoid(), +// role: 'function', +// content: `The cube is: +// \`\`\` +// ${calcEntityTypePrompt(defaultCube)} +// \`\`\` +// ` +// } +// } +// }), +// injectMakeCopilotActionable({ +// name: 'new_story_page', +// description: '', +// argumentAnnotations: [ +// { +// name: 'page', +// type: 'object', +// description: 'Page config', +// properties: zodToProperties(StoryPageSchema), +// required: true +// }, +// { +// name: 'widgets', +// type: 'array', +// description: 'Widgets in page config', +// items: { +// type: 'object', +// properties: zodToProperties(StoryWidgetSchema) +// }, +// required: true +// } +// ], +// implementation: async (page, widgets) => { +// logger.debug(`Function calling 'new_story_page', params is:`, page, widgets) +// storyService.newStoryPage({ +// ...page, +// type: StoryPointType.Canvas, +// // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) +// }) +// return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { +// Default: 'Instruction Execution Complete' +// })}` +// } +// }) ] }) } diff --git a/libs/story-angular/story/copilot/style.command.ts b/libs/story-angular/story/copilot/style.command.ts index a140ee1c7..3f7f7048b 100644 --- a/libs/story-angular/story/copilot/style.command.ts +++ b/libs/story-angular/story/copilot/style.command.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core' import { zodToProperties } from '@metad/core' -import { injectCopilotCommand, injectMakeCopilotActionable } from '@metad/copilot-angular' +import { injectCopilotCommand } from '@metad/copilot-angular' import { NxStoryService } from '@metad/story/core' import { NGXLogger } from 'ngx-logger' import { StoryStyleSchema } from './schema/story.schema' @@ -20,28 +20,28 @@ export function injectStoryStyleCommand(storyService: NxStoryService) { )}` }, actions: [ - injectMakeCopilotActionable({ - name: 'modify_story_style', - description: '', - argumentAnnotations: [ - { - name: 'style', - type: 'object', - description: 'Story styles', - properties: zodToProperties(StoryStyleSchema), - required: true - } - ], - implementation: async (style) => { - logger.debug(`Function calling 'modify_story_style', params is:`, style) - storyService.mergeStoryPreferences({ - ...style - }) - return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { - Default: 'Instruction Execution Complete' - })}` - } - }) + // injectMakeCopilotActionable({ + // name: 'modify_story_style', + // description: '', + // argumentAnnotations: [ + // { + // name: 'style', + // type: 'object', + // description: 'Story styles', + // properties: zodToProperties(StoryStyleSchema), + // required: true + // } + // ], + // implementation: async (style) => { + // logger.debug(`Function calling 'modify_story_style', params is:`, style) + // storyService.mergeStoryPreferences({ + // ...style + // }) + // return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { + // Default: 'Instruction Execution Complete' + // })}` + // } + // }) ] }) } diff --git a/packages/copilot-angular/src/lib/hooks/index.ts b/packages/copilot-angular/src/lib/hooks/index.ts index 4a1704d9f..7872f05a3 100644 --- a/packages/copilot-angular/src/lib/hooks/index.ts +++ b/packages/copilot-angular/src/lib/hooks/index.ts @@ -1,5 +1,4 @@ export * from './inject-copilot-command' -export * from './inject-make-copilot-actionable' export * from './provide-drop-action' export * from './common' export * from './prompts' diff --git a/packages/copilot-angular/src/lib/hooks/inject-make-copilot-actionable.ts b/packages/copilot-angular/src/lib/hooks/inject-make-copilot-actionable.ts index 0ddcfec9b..a131d65b5 100644 --- a/packages/copilot-angular/src/lib/hooks/inject-make-copilot-actionable.ts +++ b/packages/copilot-angular/src/lib/hooks/inject-make-copilot-actionable.ts @@ -1,29 +1,28 @@ -import { DestroyRef, inject } from '@angular/core' -import { AnnotatedFunction } from '@metad/copilot' -import { nanoid } from 'nanoid' -import { NgmCopilotContextToken } from '../services/' +// import { DestroyRef, inject } from '@angular/core' +// import { nanoid } from 'nanoid' +// import { NgmCopilotContextToken } from '../services/' -/** - * @deprecated use tools in LangChain instead - */ -export function injectMakeCopilotActionable( - annotatedFunction: AnnotatedFunction -) { - const idRef = nanoid() // generate a unique id - const copilotContext = inject(NgmCopilotContextToken) +// /** +// * @deprecated use tools in LangChain instead +// */ +// export function injectMakeCopilotActionable( +// annotatedFunction: AnnotatedFunction +// ) { +// const idRef = nanoid() // generate a unique id +// const copilotContext = inject(NgmCopilotContextToken) - const memoizedAnnotatedFunction: AnnotatedFunction = { - name: annotatedFunction.name, - description: annotatedFunction.description, - argumentAnnotations: annotatedFunction.argumentAnnotations, - implementation: annotatedFunction.implementation - } +// const memoizedAnnotatedFunction: AnnotatedFunction = { +// name: annotatedFunction.name, +// description: annotatedFunction.description, +// argumentAnnotations: annotatedFunction.argumentAnnotations, +// implementation: annotatedFunction.implementation +// } - copilotContext.setEntryPoint(idRef, memoizedAnnotatedFunction as AnnotatedFunction) +// copilotContext.setEntryPoint(idRef, memoizedAnnotatedFunction as AnnotatedFunction) - inject(DestroyRef).onDestroy(() => { - copilotContext.removeEntryPoint(idRef) - }) +// inject(DestroyRef).onDestroy(() => { +// copilotContext.removeEntryPoint(idRef) +// }) - return idRef -} +// return idRef +// } diff --git a/packages/copilot-angular/src/lib/services/context.service.ts b/packages/copilot-angular/src/lib/services/context.service.ts index 76d68b9ad..653debe10 100644 --- a/packages/copilot-angular/src/lib/services/context.service.ts +++ b/packages/copilot-angular/src/lib/services/context.service.ts @@ -1,12 +1,9 @@ import { DestroyRef, Injectable, InjectionToken, computed, inject, signal } from '@angular/core' import { ChatOpenAI, ChatOpenAICallOptions } from '@langchain/openai' import { - AnnotatedFunction, CopilotCommand, CopilotContext, CopilotContextParam, - entryPointsToChatCompletionFunctions, - entryPointsToFunctionCallHandler } from '@metad/copilot' import { AgentExecutor } from 'langchain/agents' import { Observable, firstValueFrom, map } from 'rxjs' @@ -41,33 +38,33 @@ export class NgmCopilotContextService implements CopilotContext { return commands }) - // Entry Points - /** - * @deprecated use tools instead - */ - readonly #entryPoints = signal>>({}) - - /** - * @deprecated use tools instead - */ - readonly getFunctionCallHandler = computed(() => { - return entryPointsToFunctionCallHandler(Object.values(this.#entryPoints())) - }) - /** - * @deprecated use tools instead - */ - readonly getChatCompletionFunctionDescriptions = computed(() => { - return entryPointsToChatCompletionFunctions(Object.values(this.#entryPoints())) - }) - /** - * @deprecated use tools instead - */ - readonly getGlobalFunctionDescriptions = computed(() => { - const ids = Object.keys(this.#entryPoints()).filter( - (id) => !Object.values(this.#commands()).some((command) => command.actions?.includes(id)) - ) - return entryPointsToChatCompletionFunctions(ids.map((id) => this.#entryPoints()[id])) - }) + // // Entry Points + // /** + // * @deprecated use tools instead + // */ + // readonly #entryPoints = signal>>({}) + + // /** + // * @deprecated use tools instead + // */ + // readonly getFunctionCallHandler = computed(() => { + // return entryPointsToFunctionCallHandler(Object.values(this.#entryPoints())) + // }) + // /** + // * @deprecated use tools instead + // */ + // readonly getChatCompletionFunctionDescriptions = computed(() => { + // return entryPointsToChatCompletionFunctions(Object.values(this.#entryPoints())) + // }) + // /** + // * @deprecated use tools instead + // */ + // readonly getGlobalFunctionDescriptions = computed(() => { + // const ids = Object.keys(this.#entryPoints()).filter( + // (id) => !Object.values(this.#commands()).some((command) => command.actions?.includes(id)) + // ) + // return entryPointsToChatCompletionFunctions(ids.map((id) => this.#entryPoints()[id])) + // }) /** * Contexts @@ -146,24 +143,24 @@ export class NgmCopilotContextService implements CopilotContext { }) } - setEntryPoint(id: string, entryPoint: AnnotatedFunction) { - this.#entryPoints.update((state) => ({ - ...state, - [id]: entryPoint - })) - } - - removeEntryPoint(id: string) { - this.#entryPoints.update((prevPoints) => { - const newPoints = { ...prevPoints } - delete newPoints[id] - return newPoints - }) - } - - getEntryPoint(id: string) { - return this.#entryPoints()[id] - } + // setEntryPoint(id: string, entryPoint: AnnotatedFunction) { + // this.#entryPoints.update((state) => ({ + // ...state, + // [id]: entryPoint + // })) + // } + + // removeEntryPoint(id: string) { + // this.#entryPoints.update((prevPoints) => { + // const newPoints = { ...prevPoints } + // delete newPoints[id] + // return newPoints + // }) + // } + + // getEntryPoint(id: string) { + // return this.#entryPoints()[id] + // } /** * Find command by name or alias diff --git a/packages/copilot/src/index.ts b/packages/copilot/src/index.ts index 976cee0c0..dd159dd57 100644 --- a/packages/copilot/src/index.ts +++ b/packages/copilot/src/index.ts @@ -2,7 +2,5 @@ export * from './lib/copilot' export * from './lib/command' export * from './lib/types/' export * from './lib/engine' -export * from './lib/shared/process-chat-stream' -export * from './lib/shared/functions' export * from './lib/utils' export * from './lib/graph/index' \ No newline at end of file diff --git a/packages/copilot/src/lib/shared/functions.ts b/packages/copilot/src/lib/shared/functions.ts index 43022ef61..94d471fa3 100644 --- a/packages/copilot/src/lib/shared/functions.ts +++ b/packages/copilot/src/lib/shared/functions.ts @@ -1,108 +1,107 @@ -import { ChatRequest, FunctionCall, Message, nanoid } from 'ai' -import { ChatCompletionCreateParams } from 'openai/resources' -import JSON5 from 'json5' -import { AnnotatedFunction } from '../types' +// import { ChatRequest, FunctionCall, Message, nanoid } from 'ai' +// import { ChatCompletionCreateParams } from 'openai/resources' +// import JSON5 from 'json5' -export const defaultCopilotContextCategories = ['global'] +// export const defaultCopilotContextCategories = ['global'] -/** - * @deprecated use LangChain - */ -export type FunctionCallHandler = ( - chatMessages: Message[], - functionCall: FunctionCall, - conversationId: string -) => Promise +// /** +// * @deprecated use LangChain +// */ +// export type FunctionCallHandler = ( +// chatMessages: Message[], +// functionCall: FunctionCall, +// conversationId: string +// ) => Promise -export type FunctionCallHandlerOptions = { - conversationId: string - messages: Message[] -} +// export type FunctionCallHandlerOptions = { +// conversationId: string +// messages: Message[] +// } -export function entryPointsToFunctionCallHandler(entryPoints: AnnotatedFunction[]): FunctionCallHandler { - return async (chatMessages, functionCall, conversationId): Promise => { - const entrypointsByFunctionName: Record> = {} - for (const entryPoint of entryPoints) { - entrypointsByFunctionName[entryPoint.name] = entryPoint - } +// export function entryPointsToFunctionCallHandler(entryPoints: AnnotatedFunction[]): FunctionCallHandler { +// return async (chatMessages, functionCall, conversationId): Promise => { +// const entrypointsByFunctionName: Record> = {} +// for (const entryPoint of entryPoints) { +// entrypointsByFunctionName[entryPoint.name] = entryPoint +// } - const entryPointFunction = entrypointsByFunctionName[functionCall.name || ''] - if (entryPointFunction) { - let parsedFunctionCallArguments: Record[] = [] - if (functionCall.arguments) { - parsedFunctionCallArguments = JSON5.parse(functionCall.arguments) - } +// const entryPointFunction = entrypointsByFunctionName[functionCall.name || ''] +// if (entryPointFunction) { +// let parsedFunctionCallArguments: Record[] = [] +// if (functionCall.arguments) { +// parsedFunctionCallArguments = JSON5.parse(functionCall.arguments) +// } - const paramsInCorrectOrder: any[] = [] - for (const arg of entryPointFunction.argumentAnnotations) { - paramsInCorrectOrder.push(parsedFunctionCallArguments[arg.name as keyof typeof parsedFunctionCallArguments]) - } +// const paramsInCorrectOrder: any[] = [] +// for (const arg of entryPointFunction.argumentAnnotations) { +// paramsInCorrectOrder.push(parsedFunctionCallArguments[arg.name as keyof typeof parsedFunctionCallArguments]) +// } - // return await entryPointFunction.implementation(...paramsInCorrectOrder) - const result = await entryPointFunction.implementation(...paramsInCorrectOrder, {conversationId, messages: chatMessages}) - if (!result) { - return - } - if (typeof result === 'string') { - return result - } - const functionResponse: ChatRequest = { - messages: [ - ...chatMessages, - { - ...result, - id: nanoid(), - name: functionCall.name, - role: 'function' as const - } - ] - } - return functionResponse - } - } -} +// // return await entryPointFunction.implementation(...paramsInCorrectOrder) +// const result = await entryPointFunction.implementation(...paramsInCorrectOrder, {conversationId, messages: chatMessages}) +// if (!result) { +// return +// } +// if (typeof result === 'string') { +// return result +// } +// const functionResponse: ChatRequest = { +// messages: [ +// ...chatMessages, +// { +// ...result, +// id: nanoid(), +// name: functionCall.name, +// role: 'function' as const +// } +// ] +// } +// return functionResponse +// } +// } +// } -/** - * @deprecated use LangChain - */ -export function entryPointsToChatCompletionFunctions( - entryPoints: AnnotatedFunction[] -): ChatCompletionCreateParams.Function[] { - return entryPoints.map(annotatedFunctionToChatCompletionFunction) -} +// /** +// * @deprecated use LangChain +// */ +// export function entryPointsToChatCompletionFunctions( +// entryPoints: AnnotatedFunction[] +// ): ChatCompletionCreateParams.Function[] { +// return entryPoints.map(annotatedFunctionToChatCompletionFunction) +// } -/** - * @deprecated use LangChain - */ -export function annotatedFunctionToChatCompletionFunction( - annotatedFunction: AnnotatedFunction -): ChatCompletionCreateParams.Function { - // Create the parameters object based on the argumentAnnotations - const parameters: { [key: string]: any } = {} - for (const arg of annotatedFunction.argumentAnnotations) { - // isolate the args we should forward inline - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { name, required, ...forwardedArgs } = arg - parameters[arg.name] = forwardedArgs - } +// /** +// * @deprecated use LangChain +// */ +// export function annotatedFunctionToChatCompletionFunction( +// annotatedFunction: AnnotatedFunction +// ): ChatCompletionCreateParams.Function { +// // Create the parameters object based on the argumentAnnotations +// const parameters: { [key: string]: any } = {} +// for (const arg of annotatedFunction.argumentAnnotations) { +// // isolate the args we should forward inline +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// const { name, required, ...forwardedArgs } = arg +// parameters[arg.name] = forwardedArgs +// } - const requiredParameterNames: string[] = [] - for (const arg of annotatedFunction.argumentAnnotations) { - if (arg.required) { - requiredParameterNames.push(arg.name) - } - } +// const requiredParameterNames: string[] = [] +// for (const arg of annotatedFunction.argumentAnnotations) { +// if (arg.required) { +// requiredParameterNames.push(arg.name) +// } +// } - // Create the ChatCompletionFunctions object - const chatCompletionFunction: ChatCompletionCreateParams.Function = { - name: annotatedFunction.name, - description: annotatedFunction.description, - parameters: { - type: 'object', - properties: parameters, - required: requiredParameterNames - } - } +// // Create the ChatCompletionFunctions object +// const chatCompletionFunction: ChatCompletionCreateParams.Function = { +// name: annotatedFunction.name, +// description: annotatedFunction.description, +// parameters: { +// type: 'object', +// properties: parameters, +// required: requiredParameterNames +// } +// } - return chatCompletionFunction -} +// return chatCompletionFunction +// } diff --git a/packages/copilot/src/lib/shared/process-chat-stream.ts b/packages/copilot/src/lib/shared/process-chat-stream.ts index 5cb857e11..6071deb0c 100644 --- a/packages/copilot/src/lib/shared/process-chat-stream.ts +++ b/packages/copilot/src/lib/shared/process-chat-stream.ts @@ -1,256 +1,255 @@ -/* eslint-disable no-constant-condition */ -/* eslint-disable no-inner-declarations */ -import { - ChatRequest, - JSONValue, - Message, - ToolCall, -} from 'ai'; -import { FunctionCallHandler } from './functions'; -import { CopilotChatMessage } from '../types'; - -/** - * @deprecated use LangChain - */ -export async function processChatStream({ - getStreamedResponse, - experimental_onFunctionCall, - experimental_onToolCall, - updateChatRequest, - getCurrentMessages, - conversationId -}: { - getStreamedResponse: () => Promise< - Message | { messages: Message[]; data: JSONValue[] } - >; - experimental_onFunctionCall?: FunctionCallHandler; - experimental_onToolCall?: ( - chatMessages: Message[], - toolCalls: ToolCall[], - ) => Promise; - updateChatRequest: (chatRequest: ChatRequest) => void; - getCurrentMessages: () => Message[]; - conversationId: string; -}): Promise { +// /* eslint-disable no-constant-condition */ +// /* eslint-disable no-inner-declarations */ +// import { +// ChatRequest, +// JSONValue, +// Message, +// ToolCall, +// } from 'ai'; +// import { CopilotChatMessage } from '../types'; + +// /** +// * @deprecated use LangChain +// */ +// export async function processChatStream({ +// getStreamedResponse, +// experimental_onFunctionCall, +// experimental_onToolCall, +// updateChatRequest, +// getCurrentMessages, +// conversationId +// }: { +// getStreamedResponse: () => Promise< +// Message | { messages: Message[]; data: JSONValue[] } +// >; +// experimental_onFunctionCall?: FunctionCallHandler; +// experimental_onToolCall?: ( +// chatMessages: Message[], +// toolCalls: ToolCall[], +// ) => Promise; +// updateChatRequest: (chatRequest: ChatRequest) => void; +// getCurrentMessages: () => Message[]; +// conversationId: string; +// }): Promise { - let retry = 0 - while (true) { - // TODO-STREAMDATA: This should be { const { messages: streamedResponseMessages, data } = - // await getStreamedResponse(} once Stream Data is not experimental - const messagesAndDataOrJustMessage = await getStreamedResponse(); - - // Using experimental stream data - if ('messages' in messagesAndDataOrJustMessage) { - let hasFollowingResponse = false; - - for (const message of messagesAndDataOrJustMessage.messages) { - // See if the message has a complete function call or tool call - if ( - (message.function_call === undefined || - typeof message.function_call === 'string') && - (message.tool_calls === undefined || - typeof message.tool_calls === 'string') - ) { - continue; - } - - hasFollowingResponse = true; - // Try to handle function call - if (experimental_onFunctionCall) { - const functionCall = message.function_call; - // Make sure functionCall is an object - // If not, we got tool calls instead of function calls - if (typeof functionCall !== 'object') { - console.warn( - 'experimental_onFunctionCall should not be defined when using tools', - ); - continue; - } - - // User handles the function call in their own functionCallHandler. - // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. - // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. - - const functionCallResponse: ChatRequest | string | void = - await experimental_onFunctionCall( - getCurrentMessages(), - functionCall, - conversationId - ); - - // If the user does not return anything as a result of the function call, the loop will break. - if (functionCallResponse === undefined) { - hasFollowingResponse = false; - break; - } - if (typeof functionCallResponse === 'string') { - updateChatRequest({ - messages: [ - { - ...message, - content: functionCallResponse - } - ] - }) - break - } - - if (functionCallResponse) - // A function call response was returned. - // The updated chat with function call response will be sent to the API in the next iteration of the loop. - updateChatRequest(functionCallResponse); - } - // Try to handle tool call - if (experimental_onToolCall) { - const toolCalls = message.tool_calls; - // Make sure toolCalls is an array of objects - // If not, we got function calls instead of tool calls - if ( - !Array.isArray(toolCalls) || - toolCalls.some(toolCall => typeof toolCall !== 'object') - ) { - console.warn( - 'experimental_onToolCall should not be defined when using tools', - ); - continue; - } - - // User handles the function call in their own functionCallHandler. - // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. - // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. - const toolCallResponse: ChatRequest | string | void = - await experimental_onToolCall(getCurrentMessages(), toolCalls); - - // If the user does not return anything as a result of the function call, the loop will break. - if (toolCallResponse === undefined) { - hasFollowingResponse = false; - break; - } - if (typeof toolCallResponse === 'string') { - hasFollowingResponse = false; - break - } - - if (toolCallResponse) - // A function call response was returned. - // The updated chat with function call response will be sent to the API in the next iteration of the loop. - updateChatRequest(toolCallResponse); - } - } - if (!hasFollowingResponse) { - break; - } - } else { - const streamedResponseMessage = messagesAndDataOrJustMessage; - - // TODO-STREAMDATA: Remove this once Stream Data is not experimental - if ( - (streamedResponseMessage.function_call === undefined || - typeof streamedResponseMessage.function_call === 'string') && - (streamedResponseMessage.tool_calls === undefined || - typeof streamedResponseMessage.tool_calls === 'string') - ) { - return messagesAndDataOrJustMessage - } - - // If we get here and are expecting a function call, the message should have one, if not warn and continue - if (experimental_onFunctionCall) { - const functionCall = streamedResponseMessage.function_call; - if (!(typeof functionCall === 'object')) { - console.warn( - 'experimental_onFunctionCall should not be defined when using tools', - ); - continue; - } - const functionCallResponse: ChatRequest | string | void = - await experimental_onFunctionCall(getCurrentMessages(), functionCall, conversationId); - - // If the user does not return anything as a result of the function call, the loop will break. - if (functionCallResponse === undefined) break; - if (typeof functionCallResponse === 'string') { - // Success Info! - return { - ...streamedResponseMessage, - content: functionCallResponse // `✅ Function '${functionCall.name}' call successful!` - } - } - if (retry > 3) { - break - } - retry += 1 - // Type check - if (functionCallResponse) { - // A function call response was returned. - // The updated chat with function call response will be sent to the API in the next iteration of the loop. - fixFunctionCallArguments(functionCallResponse); - updateChatRequest(functionCallResponse); - } - } - // If we get here and are expecting a tool call, the message should have one, if not warn and continue - if (experimental_onToolCall) { - const toolCalls = streamedResponseMessage.tool_calls; - if (!(typeof toolCalls === 'object')) { - console.warn( - 'experimental_onToolCall should not be defined when using functions', - ); - continue; - } - const toolCallResponse: ChatRequest | string | void = - await experimental_onToolCall(getCurrentMessages(), toolCalls); - - // If the user does not return anything as a result of the function call, the loop will break. - if (toolCallResponse === undefined) break; - if (typeof toolCallResponse === 'string') { - updateChatRequest({ - messages: [ - { - ...streamedResponseMessage, - content: toolCallResponse - } - ] - }) - break - } - // Type check - if (toolCallResponse) { - // A function call response was returned. - // The updated chat with function call response will be sent to the API in the next iteration of the loop. - fixFunctionCallArguments(toolCallResponse); - updateChatRequest(toolCallResponse); - } - } - - // Make sure function call arguments are sent back to the API as a string - function fixFunctionCallArguments(response: ChatRequest) { - for (const message of response.messages) { - if (message.tool_calls !== undefined) { - for (const toolCall of message.tool_calls) { - if (typeof toolCall === 'object') { - if ( - toolCall.function.arguments && - typeof toolCall.function.arguments !== 'string' - ) { - toolCall.function.arguments = JSON.stringify( - toolCall.function.arguments, - ); - } - } - } - } - if (message.function_call !== undefined) { - if (typeof message.function_call === 'object') { - if ( - message.function_call.arguments && - typeof message.function_call.arguments !== 'string' - ) { - message.function_call.arguments = JSON.stringify( - message.function_call.arguments, - ); - } - } - } - } - } - } - } -} +// let retry = 0 +// while (true) { +// // TODO-STREAMDATA: This should be { const { messages: streamedResponseMessages, data } = +// // await getStreamedResponse(} once Stream Data is not experimental +// const messagesAndDataOrJustMessage = await getStreamedResponse(); + +// // Using experimental stream data +// if ('messages' in messagesAndDataOrJustMessage) { +// let hasFollowingResponse = false; + +// for (const message of messagesAndDataOrJustMessage.messages) { +// // See if the message has a complete function call or tool call +// if ( +// (message.function_call === undefined || +// typeof message.function_call === 'string') && +// (message.tool_calls === undefined || +// typeof message.tool_calls === 'string') +// ) { +// continue; +// } + +// hasFollowingResponse = true; +// // Try to handle function call +// if (experimental_onFunctionCall) { +// const functionCall = message.function_call; +// // Make sure functionCall is an object +// // If not, we got tool calls instead of function calls +// if (typeof functionCall !== 'object') { +// console.warn( +// 'experimental_onFunctionCall should not be defined when using tools', +// ); +// continue; +// } + +// // User handles the function call in their own functionCallHandler. +// // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. +// // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. + +// const functionCallResponse: ChatRequest | string | void = +// await experimental_onFunctionCall( +// getCurrentMessages(), +// functionCall, +// conversationId +// ); + +// // If the user does not return anything as a result of the function call, the loop will break. +// if (functionCallResponse === undefined) { +// hasFollowingResponse = false; +// break; +// } +// if (typeof functionCallResponse === 'string') { +// updateChatRequest({ +// messages: [ +// { +// ...message, +// content: functionCallResponse +// } +// ] +// }) +// break +// } + +// if (functionCallResponse) +// // A function call response was returned. +// // The updated chat with function call response will be sent to the API in the next iteration of the loop. +// updateChatRequest(functionCallResponse); +// } +// // Try to handle tool call +// if (experimental_onToolCall) { +// const toolCalls = message.tool_calls; +// // Make sure toolCalls is an array of objects +// // If not, we got function calls instead of tool calls +// if ( +// !Array.isArray(toolCalls) || +// toolCalls.some(toolCall => typeof toolCall !== 'object') +// ) { +// console.warn( +// 'experimental_onToolCall should not be defined when using tools', +// ); +// continue; +// } + +// // User handles the function call in their own functionCallHandler. +// // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. +// // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. +// const toolCallResponse: ChatRequest | string | void = +// await experimental_onToolCall(getCurrentMessages(), toolCalls); + +// // If the user does not return anything as a result of the function call, the loop will break. +// if (toolCallResponse === undefined) { +// hasFollowingResponse = false; +// break; +// } +// if (typeof toolCallResponse === 'string') { +// hasFollowingResponse = false; +// break +// } + +// if (toolCallResponse) +// // A function call response was returned. +// // The updated chat with function call response will be sent to the API in the next iteration of the loop. +// updateChatRequest(toolCallResponse); +// } +// } +// if (!hasFollowingResponse) { +// break; +// } +// } else { +// const streamedResponseMessage = messagesAndDataOrJustMessage; + +// // TODO-STREAMDATA: Remove this once Stream Data is not experimental +// if ( +// (streamedResponseMessage.function_call === undefined || +// typeof streamedResponseMessage.function_call === 'string') && +// (streamedResponseMessage.tool_calls === undefined || +// typeof streamedResponseMessage.tool_calls === 'string') +// ) { +// return messagesAndDataOrJustMessage +// } + +// // If we get here and are expecting a function call, the message should have one, if not warn and continue +// if (experimental_onFunctionCall) { +// const functionCall = streamedResponseMessage.function_call; +// if (!(typeof functionCall === 'object')) { +// console.warn( +// 'experimental_onFunctionCall should not be defined when using tools', +// ); +// continue; +// } +// const functionCallResponse: ChatRequest | string | void = +// await experimental_onFunctionCall(getCurrentMessages(), functionCall, conversationId); + +// // If the user does not return anything as a result of the function call, the loop will break. +// if (functionCallResponse === undefined) break; +// if (typeof functionCallResponse === 'string') { +// // Success Info! +// return { +// ...streamedResponseMessage, +// content: functionCallResponse // `✅ Function '${functionCall.name}' call successful!` +// } +// } +// if (retry > 3) { +// break +// } +// retry += 1 +// // Type check +// if (functionCallResponse) { +// // A function call response was returned. +// // The updated chat with function call response will be sent to the API in the next iteration of the loop. +// fixFunctionCallArguments(functionCallResponse); +// updateChatRequest(functionCallResponse); +// } +// } +// // If we get here and are expecting a tool call, the message should have one, if not warn and continue +// if (experimental_onToolCall) { +// const toolCalls = streamedResponseMessage.tool_calls; +// if (!(typeof toolCalls === 'object')) { +// console.warn( +// 'experimental_onToolCall should not be defined when using functions', +// ); +// continue; +// } +// const toolCallResponse: ChatRequest | string | void = +// await experimental_onToolCall(getCurrentMessages(), toolCalls); + +// // If the user does not return anything as a result of the function call, the loop will break. +// if (toolCallResponse === undefined) break; +// if (typeof toolCallResponse === 'string') { +// updateChatRequest({ +// messages: [ +// { +// ...streamedResponseMessage, +// content: toolCallResponse +// } +// ] +// }) +// break +// } +// // Type check +// if (toolCallResponse) { +// // A function call response was returned. +// // The updated chat with function call response will be sent to the API in the next iteration of the loop. +// fixFunctionCallArguments(toolCallResponse); +// updateChatRequest(toolCallResponse); +// } +// } + +// // Make sure function call arguments are sent back to the API as a string +// function fixFunctionCallArguments(response: ChatRequest) { +// for (const message of response.messages) { +// if (message.tool_calls !== undefined) { +// for (const toolCall of message.tool_calls) { +// if (typeof toolCall === 'object') { +// if ( +// toolCall.function.arguments && +// typeof toolCall.function.arguments !== 'string' +// ) { +// toolCall.function.arguments = JSON.stringify( +// toolCall.function.arguments, +// ); +// } +// } +// } +// } +// if (message.function_call !== undefined) { +// if (typeof message.function_call === 'object') { +// if ( +// message.function_call.arguments && +// typeof message.function_call.arguments !== 'string' +// ) { +// message.function_call.arguments = JSON.stringify( +// message.function_call.arguments, +// ); +// } +// } +// } +// } +// } +// } +// } +// } diff --git a/packages/copilot/src/lib/types/annotated-function.ts b/packages/copilot/src/lib/types/annotated-function.ts index d767a82c8..cd898e79c 100644 --- a/packages/copilot/src/lib/types/annotated-function.ts +++ b/packages/copilot/src/lib/types/annotated-function.ts @@ -1,41 +1,41 @@ -import { CopilotChatMessage } from './types' +// import { CopilotChatMessage } from './types' -/** - * @deprecated use tools in LangChain instead - */ -export interface AnnotatedFunctionSimpleArgument { - name: string - type: 'string' | 'number' | 'boolean' | 'object' // Add or change types according to your needs. - description: string - required: boolean - properties?: any -} +// /** +// * @deprecated use tools in LangChain instead +// */ +// export interface AnnotatedFunctionSimpleArgument { +// name: string +// type: 'string' | 'number' | 'boolean' | 'object' // Add or change types according to your needs. +// description: string +// required: boolean +// properties?: any +// } -/** - * @deprecated use tools in LangChain instead - */ -export interface AnnotatedFunctionArrayArgument { - name: string - type: 'array' - items: { - type: string - properties?: any - } - description: string - required: boolean -} +// /** +// * @deprecated use tools in LangChain instead +// */ +// export interface AnnotatedFunctionArrayArgument { +// name: string +// type: 'array' +// items: { +// type: string +// properties?: any +// } +// description: string +// required: boolean +// } -/** - * @deprecated use tools in LangChain instead - */ -export type AnnotatedFunctionArgument = AnnotatedFunctionSimpleArgument | AnnotatedFunctionArrayArgument +// /** +// * @deprecated use tools in LangChain instead +// */ +// export type AnnotatedFunctionArgument = AnnotatedFunctionSimpleArgument | AnnotatedFunctionArrayArgument -/** - * @deprecated use tools in LangChain instead - */ -export interface AnnotatedFunction { - name: string - description: string - argumentAnnotations: AnnotatedFunctionArgument[] - implementation: (...args: Inputs) => Promise -} +// /** +// * @deprecated use tools in LangChain instead +// */ +// export interface AnnotatedFunction { +// name: string +// description: string +// argumentAnnotations: AnnotatedFunctionArgument[] +// implementation: (...args: Inputs) => Promise +// } diff --git a/packages/copilot/src/lib/types/index.ts b/packages/copilot/src/lib/types/index.ts index d0b06408a..c0df3880e 100644 --- a/packages/copilot/src/lib/types/index.ts +++ b/packages/copilot/src/lib/types/index.ts @@ -1,3 +1,2 @@ -export * from './annotated-function' export * from './types' export * from './providers' \ No newline at end of file From 929a11a7a6ec85b29f017f843093480876f9415e Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 17 Jul 2024 15:04:51 +0800 Subject: [PATCH 02/53] feat: ollama provider --- .../src/app/@core/copilot/agent-route.ts | 10 +- .../copilot/basic/basic.component.html | 7 +- .../setting/copilot/basic/basic.component.ts | 1 + .../copilot/calculation/agent-formula.ts | 4 +- packages/contracts/src/ai.model.ts | 3 +- .../src/lib/services/agent-free.ts | 53 +++- .../src/lib/services/engine.service.ts | 6 +- packages/copilot/src/lib/command.ts | 3 +- packages/copilot/src/lib/copilot.ts | 246 ++---------------- packages/copilot/src/lib/types/providers.ts | 32 ++- 10 files changed, 117 insertions(+), 248 deletions(-) diff --git a/apps/cloud/src/app/@core/copilot/agent-route.ts b/apps/cloud/src/app/@core/copilot/agent-route.ts index d7d9ffb38..73469088a 100644 --- a/apps/cloud/src/app/@core/copilot/agent-route.ts +++ b/apps/cloud/src/app/@core/copilot/agent-route.ts @@ -1,13 +1,13 @@ -import { BaseMessage, HumanMessage } from '@langchain/core/messages' +import { BaseChatModel } from '@langchain/core/language_models/chat_models' +import { HumanMessage } from '@langchain/core/messages' import { ChatPromptTemplate, MessagesPlaceholder, TemplateFormat } from '@langchain/core/prompts' +import { Runnable, RunnableConfig } from '@langchain/core/runnables' import { StructuredToolInterface } from '@langchain/core/tools' import { PartialValues } from '@langchain/core/utils/types' -import { Runnable, RunnableConfig } from '@langchain/core/runnables' -import { ChatOpenAI } from '@langchain/openai' +import { createCopilotAgentState } from '@metad/copilot' import { AgentState } from '@metad/copilot-angular' import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents' import { z } from 'zod' -import { createCopilotAgentState } from '@metad/copilot' type ZodAny = z.ZodObject @@ -48,7 +48,7 @@ export function createState() { * @returns */ export async function createWorkerAgent( - llm: ChatOpenAI, + llm: BaseChatModel, tools: StructuredToolInterface[], systemPrompt: string, partialValues?: PartialValues, diff --git a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html index 6f1143d23..f285f250e 100644 --- a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html +++ b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html @@ -7,9 +7,10 @@ {{ 'PAC.Copilot.Provider' | translate: {Default: 'Provider'} }} - OpenAI - Azure - + OpenAI + Azure + Ollama + {{ 'PAC.Copilot.Provider_DashScope' | translate: {Default: 'DashScope'} }} diff --git a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.ts b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.ts index fae7bba32..02db97992 100644 --- a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.ts +++ b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.ts @@ -15,6 +15,7 @@ import { MaterialModule, TranslationBaseComponent } from '../../../../@shared' imports: [TranslateModule, MaterialModule, FormsModule, ReactiveFormsModule] }) export class CopilotBasicComponent extends TranslationBaseComponent { + AiProvider = AiProvider readonly copilotService = inject(PACCopilotService) readonly _toastrService = inject(ToastrService) diff --git a/apps/cloud/src/app/features/story/copilot/calculation/agent-formula.ts b/apps/cloud/src/app/features/story/copilot/calculation/agent-formula.ts index b4dfb70f9..29c68fa62 100644 --- a/apps/cloud/src/app/features/story/copilot/calculation/agent-formula.ts +++ b/apps/cloud/src/app/features/story/copilot/calculation/agent-formula.ts @@ -1,9 +1,9 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { DynamicStructuredTool } from '@langchain/core/tools' -import { ChatOpenAI } from '@langchain/openai' import { makeCubeRulesPrompt } from '@metad/core'; import { Route } from 'apps/cloud/src/app/@core/copilot' -export async function createFormulaWorker({ llm, tools }: { llm: ChatOpenAI; tools: DynamicStructuredTool[] }) { +export async function createFormulaWorker({ llm, tools }: { llm: BaseChatModel; tools: DynamicStructuredTool[] }) { const systemPrompt = `You are a data analyst. Please use MDX expressions to create a calculated measure for a cube.` + ` The name of new calculation measure should be unique with existing measures.` + ` Use the dimensions, hierarchy, level and other names accurately according to the cube information provided.` + diff --git a/packages/contracts/src/ai.model.ts b/packages/contracts/src/ai.model.ts index d8d18e521..3c5d0723d 100644 --- a/packages/contracts/src/ai.model.ts +++ b/packages/contracts/src/ai.model.ts @@ -4,7 +4,8 @@ export enum AiProvider { OpenAI = 'openai', Azure = 'azure', - DashScope = 'dashscope' + DashScope = 'dashscope', + Ollama = 'ollama' } /** diff --git a/packages/copilot-angular/src/lib/services/agent-free.ts b/packages/copilot-angular/src/lib/services/agent-free.ts index 1308454d6..bf17eda62 100644 --- a/packages/copilot-angular/src/lib/services/agent-free.ts +++ b/packages/copilot-angular/src/lib/services/agent-free.ts @@ -1,10 +1,11 @@ -import { SystemMessage } from '@langchain/core/messages' +import { ChatPromptTemplate } from '@langchain/core/prompts' +import { RunnableLambda } from '@langchain/core/runnables' import { DynamicStructuredTool } from '@langchain/core/tools' -import { StateGraphArgs } from '@langchain/langgraph/web' -import { AgentState, CreateGraphOptions, createReactAgent, Team } from '@metad/copilot' +import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web' +import { AgentState, createCopilotAgentState, CreateGraphOptions } from '@metad/copilot' import { z } from 'zod' -const superState: StateGraphArgs['channels'] = Team.createState() +const state: StateGraphArgs["channels"] = createCopilotAgentState() const dummyTool = new DynamicStructuredTool({ name: 'dummy', @@ -17,16 +18,42 @@ const dummyTool = new DynamicStructuredTool({ export function injectCreateChatAgent() { return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { - return createReactAgent({ - state: superState, - llm, - tools: [dummyTool], - checkpointSaver: checkpointer, + + const prompt = ChatPromptTemplate.fromMessages([ + ['system', '{role}'], + ["placeholder", "{messages}"], + ]); + + const callModel = async (state: AgentState) => { + // TODO: Auto-promote streaming. + return { messages: [await prompt.pipe(llm).invoke(state)] }; + }; + const workflow = new StateGraph({ + channels: state + }) + .addNode( + "agent", + new RunnableLambda({ func: callModel }).withConfig({ runName: "agent" }) + ) + .addEdge('agent', END) + .addEdge(START, "agent") + + return workflow.compile({ + checkpointer, interruptBefore, interruptAfter, - messageModifier: async (state) => { - return [new SystemMessage(`${state.role}\n${state.context}`), ...state.messages] - } - }) + }); + + // return createReactAgent({ + // state: superState, + // llm, + // tools: [], + // checkpointSaver: checkpointer, + // interruptBefore, + // interruptAfter, + // messageModifier: async (state) => { + // return [new SystemMessage(`${state.role}\n${state.context}`), ...state.messages] + // } + // }) } } diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index 905e1681e..d1dd0130b 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -629,10 +629,14 @@ export class NgmCopilotEngineService implements CopilotEngine { this.upsertMessage({ id: assistantId, role: CopilotChatMessageRoleEnum.Assistant, - content: '', + // content: '', status: 'error', error: err.message }) + this.updateConversation(conversation.id, (conversation) => ({ + ...conversation, + status: 'error' + })) return } } diff --git a/packages/copilot/src/lib/command.ts b/packages/copilot/src/lib/command.ts index d1a0d6488..df8c1c514 100644 --- a/packages/copilot/src/lib/command.ts +++ b/packages/copilot/src/lib/command.ts @@ -1,3 +1,4 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models' import { BaseStringPromptTemplate, ChatPromptTemplate } from '@langchain/core/prompts' import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools' import { BaseCheckpointSaver, CompiledStateGraph, StateGraph } from '@langchain/langgraph/web' @@ -80,7 +81,7 @@ export interface CopilotCommand { } export type CreateGraphOptions = { - llm: ChatOpenAI; + llm: ChatOpenAI checkpointer?: BaseCheckpointSaver interruptBefore?: any[] interruptAfter?: any[] diff --git a/packages/copilot/src/lib/copilot.ts b/packages/copilot/src/lib/copilot.ts index a58ba4a47..3eb4507c8 100644 --- a/packages/copilot/src/lib/copilot.ts +++ b/packages/copilot/src/lib/copilot.ts @@ -1,28 +1,26 @@ import { ChatOpenAI, ClientOptions } from '@langchain/openai' -import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source' -import { UseChatOptions as AiUseChatOptions, ChatRequest, ChatRequestOptions, JSONValue, Message, nanoid } from 'ai' -import { BehaviorSubject, Observable, catchError, combineLatest, map, of, shareReplay, switchMap, throwError } from 'rxjs' +import { ChatOllama } from "@langchain/community/chat_models/ollama" +import { OllamaFunctions } from "@langchain/community/experimental/chat_models/ollama_functions"; + +// import { UseChatOptions as AiUseChatOptions, Message } from 'ai' +import { BehaviorSubject, catchError, combineLatest, map, of, shareReplay, switchMap } from 'rxjs' import { fromFetch } from 'rxjs/fetch' -import { callChatApi as callDashScopeChatApi } from './dashscope/' -import { callChatApi } from './shared/call-chat-api' import { AI_PROVIDERS, AiProvider, BusinessRoleType, - CopilotChatMessage, - DefaultModel, ICopilot, RequestOptions } from './types' -function chatCompletionsUrl(copilot: ICopilot) { - const apiHost: string = copilot.apiHost || AI_PROVIDERS[copilot.provider]?.apiHost - const chatCompletionsUrl: string = AI_PROVIDERS[copilot.provider]?.chatCompletionsUrl - return ( - copilot.chatUrl || - (apiHost?.endsWith('/') ? apiHost.slice(0, apiHost.length - 1) + chatCompletionsUrl : apiHost + chatCompletionsUrl) - ) -} +// function chatCompletionsUrl(copilot: ICopilot) { +// const apiHost: string = copilot.apiHost || AI_PROVIDERS[copilot.provider]?.apiHost +// const chatCompletionsUrl: string = AI_PROVIDERS[copilot.provider]?.chatCompletionsUrl +// return ( +// copilot.chatUrl || +// (apiHost?.endsWith('/') ? apiHost.slice(0, apiHost.length - 1) + chatCompletionsUrl : apiHost + chatCompletionsUrl) +// ) +// } function modelsUrl(copilot: ICopilot) { const apiHost: string = copilot.apiHost || AI_PROVIDERS[copilot.provider]?.apiHost @@ -33,11 +31,11 @@ function modelsUrl(copilot: ICopilot) { ) } -export type UseChatOptions = AiUseChatOptions & { - appendMessage?: (message: Message) => void - abortController?: AbortController | null - model: string -} +// export type UseChatOptions = AiUseChatOptions & { +// appendMessage?: (message: Message) => void +// abortController?: AbortController | null +// model: string +// } /** * Copilot Service @@ -84,6 +82,14 @@ export abstract class CopilotService { model: copilot.defaultModel, temperature: 0, }) + case AiProvider.Ollama: + return new OllamaFunctions({ + baseUrl: copilot.apiHost || null, + model: copilot.defaultModel, + headers: { + ...(clientOptions?.defaultHeaders ?? {}) + } + }) as unknown as ChatOpenAI default: return null } @@ -112,140 +118,6 @@ export abstract class CopilotService { return {} } - // /** - // * @deprecated use langchain/openai instead - // */ - // async createChat( - // messages: CopilotChatMessage[], - // options?: { - // request?: any - // signal?: AbortSignal - // } - // ) { - // const { request, signal } = options ?? {} - // const response = await fetch(chatCompletionsUrl(this.copilot), { - // method: 'POST', - // headers: { - // 'content-type': 'application/json', - // ...((this.requestOptions()?.headers ?? {}) as Record) - // }, - // signal, - // body: JSON.stringify({ - // model: DefaultModel, - // messages: messages.map((message) => ({ - // role: message.role, - // content: message.content - // })), - // ...(request ?? {}) - // }) - // }) - - // if (response.status === 200) { - // const answer = await response.json() - // return answer.choices - // } - - // throw new Error((await response.json()).error?.message) - // } - - // /** - // * @deprecated use langchain/openai instead - // */ - // chatCompletions(messages: CopilotChatMessage[], request?: any): Observable { - // return fromFetch(chatCompletionsUrl(this.copilot), { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // ...((this.requestOptions()?.headers ?? {}) as Record) - // // Authorization: `Bearer ${this.copilot.apiKey}` - // }, - // body: JSON.stringify({ - // model: DefaultModel, - // messages: messages.map((message) => ({ - // role: message.role, - // content: message.content - // })), - // ...(request ?? {}) - // }) - // }).pipe( - // switchMap((response) => { - // if (response.ok) { - // // OK return data - // return response.json() - // } else { - // // Server is returning a status requiring the client to try something else. - - // return throwError(() => `Error ${response.status}`) - // } - // }) - // ) - // } - - // /** - // * @deprecated use langchain/openai instead - // */ - // chatStream(messages: CopilotChatMessage[], request?: any) { - // return new Observable((subscriber) => { - // const ctrl = new AbortController() - // fetchEventSource(chatCompletionsUrl(this.copilot), { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // ...((this.requestOptions()?.headers ?? {}) as Record) - // // Authorization: `Bearer ${this.copilot.apiKey}` - // }, - // body: JSON.stringify({ - // model: DefaultModel, - // messages: messages.map((message) => ({ - // role: message.role, - // content: message.content - // })), - // ...(request ?? {}), - // stream: true - // }), - // signal: ctrl.signal, - // async onopen(response) { - // if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - // return // everything's good - // } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { - // // client-side errors are usually non-retriable: - // subscriber.error(response.status) - // } else { - // subscriber.error(response.status) - // } - // }, - // onmessage(msg) { - // // if the server emits an error message, throw an exception - // // so it gets handled by the onerror callback below: - // if (msg.event === 'FatalError') { - // throw new Error(msg.data) - // } - - // if (msg.data === `"[DONE]"` || msg.data === '[DONE]') { - // subscriber.complete() - // } else { - // try { - // subscriber.next(JSON.parse(msg.data)) - // } catch (err) { - // subscriber.error(err) - // } - // } - // }, - // onclose() { - // // if the server closes the connection unexpectedly, retry: - // // throw new Error() - // subscriber.complete() - // }, - // onerror(err) { - // subscriber.error(err) - // ctrl.abort() - // } - // }) - - // return () => ctrl.abort() - // }) - // } - getModels() { return fromFetch(modelsUrl(this.copilot), { method: 'GET', @@ -272,70 +144,4 @@ export abstract class CopilotService { ) } - // /** - // * @deprecated use langchain/openai instead - // */ - // async chat( - // { - // sendExtraMessageFields, - // onResponse, - // onFinish, - // onError, - // appendMessage, - // credentials, - // headers, - // body, - // generateId = nanoid, - // abortController, - // model - // }: UseChatOptions = { - // model: DefaultModel - // }, - // chatRequest: ChatRequest, - // { options, data }: ChatRequestOptions = {} - // ): Promise { - // const callChatApiFuc = this.copilot.provider === AiProvider.DashScope ? callDashScopeChatApi : callChatApi - // return await callChatApiFuc({ - // api: chatCompletionsUrl(this.copilot), - // model, - // chatRequest, - // messages: sendExtraMessageFields - // ? chatRequest.messages - // : chatRequest.messages.map(({ role, content, name, function_call }) => ({ - // role, - // content, - // ...(name !== undefined && { name }), - // ...(function_call !== undefined && { - // function_call - // }) - // })), - // body: { - // data: chatRequest.data, - // ...body, - // ...(options?.body ?? {}) - // }, - // headers: { - // ...(this.requestOptions()?.headers ?? {}), - // ...headers, - // ...(options?.headers ?? {}) - // }, - // abortController: () => abortController, - // credentials, - // onResponse, - // onUpdate(merged, data) { - // console.log(`onUpdate`, merged, data) - // // mutate([...chatRequest.messages, ...merged]); - // // setStreamData([...existingData, ...(data ?? [])]); - // }, - // onFinish, - // appendMessage, - // restoreMessagesOnFailure() { - // // Restore the previous messages if the request fails. - // // if (previousMessages.status === 'success') { - // // mutate(previousMessages.data); - // // } - // }, - // generateId - // }) - // } } diff --git a/packages/copilot/src/lib/types/providers.ts b/packages/copilot/src/lib/types/providers.ts index a9d91053e..942886dd3 100644 --- a/packages/copilot/src/lib/types/providers.ts +++ b/packages/copilot/src/lib/types/providers.ts @@ -1,7 +1,23 @@ +/** + * Providers for LLMs + * + * - https://js.langchain.com/v0.2/docs/integrations/chat/ + */ export enum AiProvider { + /** + * - https://js.langchain.com/v0.2/docs/integrations/chat/openai + */ OpenAI = 'openai', + /** + * - https://js.langchain.com/v0.2/docs/integrations/chat/azure + */ Azure = 'azure', - DashScope = 'dashscope' + DashScope = 'dashscope', + /** + * - https://ollama.com/ + * - https://js.langchain.com/v0.2/docs/integrations/chat/ollama + */ + Ollama = 'ollama' } export type AiModelType = { @@ -20,7 +36,7 @@ export type AiProviderType = { isTools: boolean } -export const AI_PROVIDERS: Record = { +export const AI_PROVIDERS: Record> = { [AiProvider.OpenAI]: { apiHost: 'https://api.openai.com/v1', chatCompletionsUrl: '/chat/completions', @@ -144,5 +160,17 @@ export const AI_PROVIDERS: Record = { name: '百川2 7b v1' } ] + }, + [AiProvider.Ollama]: { + models: [ + { + id: 'llama2', + name: 'LLama 3' + }, + { + id: 'qwen2', + name: 'Qwen 2' + } + ] } } From 3e44ef3a3c993d433948d1d0fd20b5199456367a Mon Sep 17 00:00:00 2001 From: meta-d Date: Fri, 19 Jul 2024 16:09:50 +0800 Subject: [PATCH 03/53] feat: ai provider role --- .../src/lib/copilot/control/chat.ts | 22 ++-- packages/contracts/src/copilot.model.ts | 6 + .../src/lib/types/annotated-function.ts | 41 ------- packages/copilot/src/lib/types/providers.ts | 4 + packages/copilot/src/lib/types/types.ts | 108 +++++++++--------- packages/copilot/src/lib/utils.ts | 22 ++++ packages/server/src/ai/providers.ts | 4 +- packages/server/src/copilot/copilot.entity.ts | 8 +- 8 files changed, 104 insertions(+), 111 deletions(-) delete mode 100644 packages/copilot/src/lib/types/annotated-function.ts diff --git a/libs/story-angular/src/lib/copilot/control/chat.ts b/libs/story-angular/src/lib/copilot/control/chat.ts index 38614fe19..6fe2608f6 100644 --- a/libs/story-angular/src/lib/copilot/control/chat.ts +++ b/libs/story-angular/src/lib/copilot/control/chat.ts @@ -1,4 +1,4 @@ -import { CopilotChatMessageRoleEnum, getFunctionCall } from '@metad/copilot' +import { CopilotChatMessageRoleEnum } from '@metad/copilot' import { calcEntityTypePrompt } from '@metad/core' import { DataSettings, assignDeepOmitBlank, cloneDeep, omit, omitBlank } from '@metad/ocap-core' import { StoryWidget, WidgetComponentType } from '@metad/story/core' @@ -34,16 +34,16 @@ Original widget is ${JSON.stringify(widget)}` ...omitBlank(copilot.options) } ) - .pipe( - map(({ choices }) => { - try { - copilot.response = getFunctionCall(choices[0].message) - } catch (err) { - copilot.error = err as Error - } - return copilot - }) - ) + // .pipe( + // map(({ choices }) => { + // try { + // copilot.response = getFunctionCall(choices[0].message) + // } catch (err) { + // copilot.error = err as Error + // } + // return copilot + // }) + // ) } export function editControlWidgetCommand(copilot) { diff --git a/packages/contracts/src/copilot.model.ts b/packages/contracts/src/copilot.model.ts index 7967cf5fe..35dcb8a38 100644 --- a/packages/contracts/src/copilot.model.ts +++ b/packages/contracts/src/copilot.model.ts @@ -2,6 +2,7 @@ import { AiProvider } from "./ai.model" import { IBasePerTenantAndOrganizationEntityModel } from "./base-entity.model" export interface ICopilot extends IBasePerTenantAndOrganizationEntityModel { + role: AiProviderRole enabled?: boolean provider?: AiProvider apiKey?: string @@ -15,3 +16,8 @@ export interface ICopilot extends IBasePerTenantAndOrganizationEntityModel { */ options?: any } + +export enum AiProviderRole { + Primary = 'primary', + Secondary ='secondary' +} \ No newline at end of file diff --git a/packages/copilot/src/lib/types/annotated-function.ts b/packages/copilot/src/lib/types/annotated-function.ts deleted file mode 100644 index cd898e79c..000000000 --- a/packages/copilot/src/lib/types/annotated-function.ts +++ /dev/null @@ -1,41 +0,0 @@ -// import { CopilotChatMessage } from './types' - -// /** -// * @deprecated use tools in LangChain instead -// */ -// export interface AnnotatedFunctionSimpleArgument { -// name: string -// type: 'string' | 'number' | 'boolean' | 'object' // Add or change types according to your needs. -// description: string -// required: boolean -// properties?: any -// } - -// /** -// * @deprecated use tools in LangChain instead -// */ -// export interface AnnotatedFunctionArrayArgument { -// name: string -// type: 'array' -// items: { -// type: string -// properties?: any -// } -// description: string -// required: boolean -// } - -// /** -// * @deprecated use tools in LangChain instead -// */ -// export type AnnotatedFunctionArgument = AnnotatedFunctionSimpleArgument | AnnotatedFunctionArrayArgument - -// /** -// * @deprecated use tools in LangChain instead -// */ -// export interface AnnotatedFunction { -// name: string -// description: string -// argumentAnnotations: AnnotatedFunctionArgument[] -// implementation: (...args: Inputs) => Promise -// } diff --git a/packages/copilot/src/lib/types/providers.ts b/packages/copilot/src/lib/types/providers.ts index 942886dd3..41623b8c1 100644 --- a/packages/copilot/src/lib/types/providers.ts +++ b/packages/copilot/src/lib/types/providers.ts @@ -55,6 +55,10 @@ export const AI_PROVIDERS: Record> = { id: 'gpt-4o', name: 'GPT-4 Omni' }, + { + id: 'gpt-4o-mini', + name: 'GPT-4 O mini' + }, { id: 'gpt-4-turbo', name: 'GPT-4 Turbo' diff --git a/packages/copilot/src/lib/types/types.ts b/packages/copilot/src/lib/types/types.ts index f189d302b..494fd94b1 100644 --- a/packages/copilot/src/lib/types/types.ts +++ b/packages/copilot/src/lib/types/types.ts @@ -1,15 +1,18 @@ -import { BaseMessage } from '@langchain/core/messages' -import { Message } from 'ai' -import JSON5 from 'json5' -import { ChatCompletionMessage } from 'openai/resources' -import { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions' +import { BaseMessage, FunctionCall, OpenAIToolCall } from '@langchain/core/messages' import { AiProvider } from './providers' +import { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions' export const DefaultModel = 'gpt-3.5-turbo' export const DefaultBusinessRole = 'default' +export enum AiProviderRole { + Primary = 'primary', + Secondary = 'secondary' +} + export interface ICopilot { enabled: boolean + role: AiProviderRole provider?: AiProvider /** * Authorization key for the API @@ -54,9 +57,34 @@ export enum CopilotChatMessageRoleEnum { } /** - * @deprecated remove Message from `ai` package */ -export interface CopilotChatMessage extends Omit { +export interface CopilotChatMessage { + id: string + tool_call_id?: string + createdAt?: Date + content: string + /** + * If the message has a role of `function`, the `name` field is the name of the function. + * Otherwise, the name field should not be set. + */ + name?: string + /** + * If the assistant role makes a function call, the `function_call` field + * contains the function call name and arguments. Otherwise, the field should + * not be set. (Deprecated and replaced by tool_calls.) + */ + function_call?: string | FunctionCall + data?: JSONValue + /** + * If the assistant role makes a tool call, the `tool_calls` field contains + * the tool call name and arguments. Otherwise, the field should not be set. + */ + tool_calls?: string | OpenAIToolCall[] + /** + * Additional message-specific information added on the server via StreamData + */ + annotations?: JSONValue[] | undefined + error?: string role: 'system' | 'user' | 'assistant' | 'function' | 'data' | 'tool' | 'info' @@ -87,61 +115,11 @@ export interface CopilotChatResponseChoice { // } -/** - * @deprecated use LangChain - */ -export type AIOptions = ChatCompletionCreateParamsBase & { - useSystemPrompt?: boolean - verbose?: boolean - interactive?: boolean -} - -// Helper function -/** - * @deprecated use LangChain - */ -export function getFunctionCall(message: ChatCompletionMessage, name?: string) { - if (message.role !== CopilotChatMessageRoleEnum.Assistant) { - throw new Error('Only assistant messages can be used to generate function calls') - } - - if (name && name !== message.function_call.name) { - throw new Error(`The message is not the function call '${name}'`) - } - - return { - name: message.function_call.name, - arguments: JSON5.parse(message.function_call.arguments) - } -} - -/** - * Split the prompt into command and prompt - * - * @param prompt - * @returns - */ -export function getCommandPrompt(prompt: string) { - prompt = prompt.trim() - // a regex match `/command prompt` - const match = prompt.match(/^\/([a-zA-Z\-]*)\s*/i) - const command = match?.[1] - - return { - command, - prompt: command ? prompt.replace(`/${command}`, '').trim() : prompt - } -} - export const CopilotDefaultOptions = { model: 'gpt-3.5-turbo-0613', temperature: 0.2 } -export function nonNullable(value: T): value is NonNullable { - return value != null -} - export type BusinessRoleType = { name: string title: string @@ -154,3 +132,19 @@ export type RequestOptions = { headers?: Record | Headers body?: object } + +type JSONValue = + | null + | string + | number + | boolean + | { + [x: string]: JSONValue + } + | Array + +export type AIOptions = ChatCompletionCreateParamsBase & { + useSystemPrompt?: boolean + verbose?: boolean + interactive?: boolean +} \ No newline at end of file diff --git a/packages/copilot/src/lib/utils.ts b/packages/copilot/src/lib/utils.ts index c384052bb..6da5b3864 100644 --- a/packages/copilot/src/lib/utils.ts +++ b/packages/copilot/src/lib/utils.ts @@ -8,4 +8,26 @@ export function zodToAnnotations(obj: ZodType) { export function nanoid() { return _nanoid() +} + +export function nonNullable(value: T): value is NonNullable { + return value != null +} + +/** + * Split the prompt into command and prompt + * + * @param prompt + * @returns + */ +export function getCommandPrompt(prompt: string) { + prompt = prompt.trim() + // a regex match `/command prompt` + const match = prompt.match(/^\/([a-zA-Z\-]*)\s*/i) + const command = match?.[1] + + return { + command, + prompt: command ? prompt.replace(`/${command}`, '').trim() : prompt + } } \ No newline at end of file diff --git a/packages/server/src/ai/providers.ts b/packages/server/src/ai/providers.ts index 9312ac2e7..b64833345 100644 --- a/packages/server/src/ai/providers.ts +++ b/packages/server/src/ai/providers.ts @@ -16,7 +16,7 @@ export type AiProviderType = { isTools: boolean } -export const AI_PROVIDERS: Record = { +export const AI_PROVIDERS: Record> = { [AiProvider.OpenAI]: { apiHost: 'https://api.openai.com', chatCompletionsUrl: '/chat/completions', @@ -124,5 +124,7 @@ export const AI_PROVIDERS: Record = { name: '百川2 7b v1' } ] + }, + [AiProvider.Ollama]: { } } diff --git a/packages/server/src/copilot/copilot.entity.ts b/packages/server/src/copilot/copilot.entity.ts index f1a137661..e4068a045 100644 --- a/packages/server/src/copilot/copilot.entity.ts +++ b/packages/server/src/copilot/copilot.entity.ts @@ -1,4 +1,4 @@ -import { AiProvider, ICopilot } from '@metad/contracts' +import { AiProvider, AiProviderRole, ICopilot } from '@metad/contracts' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { IsOptional, IsString, IsBoolean, IsJSON } from 'class-validator' import { AfterLoad, Column, Entity } from 'typeorm' @@ -14,6 +14,12 @@ export class Copilot extends TenantOrganizationBaseEntity implements ICopilot { @Column({ default: false }) enabled?: boolean + @ApiPropertyOptional({ type: () => String }) + @IsString() + @IsOptional() + @Column({ nullable: true, length: 10 }) + role: AiProviderRole + @ApiPropertyOptional({ type: () => String }) @IsString() @IsOptional() From 73b3bfa3feaa612107a082cc6747123e400f22dc Mon Sep 17 00:00:00 2001 From: meta-d Date: Fri, 19 Jul 2024 19:00:52 +0800 Subject: [PATCH 04/53] feat: copilot secondary role --- .../src/app/@core/services/copilot.service.ts | 111 +++++++--------- .../copilot/basic/basic.component.html | 62 ++++++++- .../copilot/basic/basic.component.scss | 3 + .../setting/copilot/basic/basic.component.ts | 66 ++++++--- .../setting/copilot/copilot.component.scss | 2 +- .../setting/copilot/copilot.component.ts | 99 +------------- .../src/lib/services/agent-free.ts | 6 +- .../src/lib/services/context.service.ts | 8 +- .../src/lib/services/engine.service.ts | 125 +----------------- packages/copilot/src/lib/command.ts | 1 + packages/copilot/src/lib/copilot.ts | 110 +++++++-------- packages/server/src/ai/ai.controller.ts | 32 ++--- .../server/src/copilot/copilot.controller.ts | 8 +- .../server/src/copilot/copilot.service.ts | 27 ++++ 14 files changed, 269 insertions(+), 391 deletions(-) diff --git a/apps/cloud/src/app/@core/services/copilot.service.ts b/apps/cloud/src/app/@core/services/copilot.service.ts index c936c572f..68a466572 100644 --- a/apps/cloud/src/app/@core/services/copilot.service.ts +++ b/apps/cloud/src/app/@core/services/copilot.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http' import { effect, inject, Injectable, signal } from '@angular/core' import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { API_PREFIX, AuthService } from '@metad/cloud/state' -import { BusinessRoleType, ICopilot, RequestOptions } from '@metad/copilot' +import { AiProviderRole, BusinessRoleType, ICopilot, RequestOptions } from '@metad/copilot' import { NgmCopilotService } from '@metad/copilot-angular' import { environment } from 'apps/cloud/src/environments/environment' import { omit } from 'lodash-es' @@ -22,7 +22,7 @@ export class PACCopilotService extends NgmCopilotService { readonly authService = inject(AuthService) readonly roleService = inject(CopilotRoleService) - readonly copilotConfig = signal(null) + readonly copilots = signal(null) // Init copilot config private _userSub = this.#store.user$ @@ -36,20 +36,17 @@ export class PACCopilotService extends NgmCopilotService { takeUntilDestroyed() ) .subscribe((result) => { - if (result.total > 0) { - this.copilotConfig.set(result.items[0]) - this.copilot = { - ...result.items[0], - chatUrl: API_CHAT, - apiHost: API_AI_HOST, - apiKey: this.#store.token - } - } else { - this.copilotConfig.set(null) - this.copilot = { - enabled: false - } - } + this.copilots.set(result.items) + + // if (result.total > 0) { + // this.copilotConfig.set(result.items[0]) + + // } else { + // this.copilotConfig.set(null) + // this.copilot = { + // enabled: false + // } + // } }) private roleSub = this.roleService @@ -93,6 +90,31 @@ export class PACCopilotService extends NgmCopilotService { constructor() { super() + effect(() => { + const items = this.copilots() + if (items?.length > 0) { + items.forEach((item) => { + if (item.role === AiProviderRole.Primary) { + this.copilot = { + ...item, + chatUrl: API_CHAT, + apiHost: API_AI_HOST + `/${AiProviderRole.Primary}`, + apiKey: this.#store.token + } + } else if (item.role === AiProviderRole.Secondary) { + this.secondary = { + ...item, + apiHost: API_AI_HOST + `/${AiProviderRole.Secondary}` + } + } + }) + } else { + this.copilot = { + enabled: false + } + } + }, { allowSignalWrites: true }) + effect( () => { if (this.#store.copilotRole()) { @@ -110,58 +132,25 @@ export class PACCopilotService extends NgmCopilotService { ) } - override requestOptions(): RequestOptions { - return { - headers: { - 'Organization-Id': `${this.#store.selectedOrganization?.id}`, - Authorization: this.getAuthorizationToken() - } - } - } - - private getAuthorizationToken() { - return `Bearer ${this.#store.token}` - } - - // getClientOptions() { + // override requestOptions(): RequestOptions { // return { - // defaultHeaders: { + // headers: { // 'Organization-Id': `${this.#store.selectedOrganization?.id}`, // Authorization: this.getAuthorizationToken() - // }, - // fetch: async (url: string, request: RequestInit) => { - // try { - // const response = await fetch(url, request) - // // Refresh token if unauthorized - // if (response.status === 401) { - // try { - // await firstValueFrom(this.authService.isAlive()) - // request.headers['authorization'] = this.getAuthorizationToken() - // return await fetch(url, request) - // } catch (error) { - // return response - // } - // } - - // return response - // } catch (error) { - // console.error(error) - // return null; - // } // } // } // } - async upsertOne(input: Partial) { - const copilot = await firstValueFrom( - this.httpClient.post(API_PREFIX + '/copilot', input.id ? input : omit(input, 'id')) - ) - this.copilotConfig.set(copilot) - this.copilot = { - ...copilot, - chatUrl: API_CHAT, - apiHost: API_AI_HOST - } + private getAuthorizationToken() { + return `Bearer ${this.#store.token}` + } + + async upsertItems(items: Partial) { + items = await Promise.all(items.map((item) => firstValueFrom( + this.httpClient.post(API_PREFIX + '/copilot', item.id ? item : omit(item, 'id')) + ))) + + this.copilots.set(items as ICopilot[]) } } diff --git a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html index f285f250e..3b45379fe 100644 --- a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html +++ b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html @@ -9,10 +9,6 @@ OpenAI Azure - Ollama - - {{ 'PAC.Copilot.Provider_DashScope' | translate: {Default: 'DashScope'} }} - @@ -23,7 +19,7 @@ {{ 'PAC.Copilot.APIKey' | translate: {Default: 'API Key'} }} + autocomplete="ai-token"> @@ -58,6 +54,62 @@ +

Secondary

+
+
- @if(copilotEnabled$()) { diff --git a/libs/apps/indicator-market/src/lib/indicator-market.component.html b/libs/apps/indicator-market/src/lib/indicator-market.component.html index a42a662f4..1eb8108ff 100644 --- a/libs/apps/indicator-market/src/lib/indicator-market.component.html +++ b/libs/apps/indicator-market/src/lib/indicator-market.component.html @@ -52,7 +52,7 @@
- + { readonly tagType = this.indicatorsStore.tagType - readonly indicators = this.indicatorsStore.indicators + readonly indicators$ = this.indicatorsStore.indicators$ readonly mediaMatcher$ = combineLatest( Object.keys(Breakpoints).map((name) => { return this.breakpointObserver @@ -198,7 +198,7 @@ ${this.indicatorDetailComponent()?.makeIndicatorDataPrompt()} } onSearch(event) { - this.indicatorsStore.updateSearch((event.target.value)?.toLowerCase()) + this.indicatorsStore.updateSearch(event.target.value) } trackById(index, el) { diff --git a/libs/apps/indicator-market/src/lib/services/store.ts b/libs/apps/indicator-market/src/lib/services/store.ts index d78e2ac84..d76e50d26 100644 --- a/libs/apps/indicator-market/src/lib/services/store.ts +++ b/libs/apps/indicator-market/src/lib/services/store.ts @@ -16,8 +16,8 @@ import { ComponentStore } from '@metad/store' import { StoryModel, convertStoryModel2DataSource } from '@metad/story/core' import { EntityAdapter, EntityState, Update, createEntityAdapter } from '@ngrx/entity' import { assign, includes, indexOf, isEmpty, isEqual, sortBy, uniq } from 'lodash-es' -import { Observable, Subject, combineLatest, firstValueFrom } from 'rxjs' -import { distinctUntilChanged, filter, map, startWith, switchMap, tap } from 'rxjs/operators' +import { BehaviorSubject, Observable, Subject, combineLatest, firstValueFrom } from 'rxjs' +import { debounceTime, distinctUntilChanged, filter, map, startWith, switchMap, tap } from 'rxjs/operators' import { IndicatorState, IndicatorTagEnum, LookbackDefault } from '../types' import { TranslateService } from '@ngx-translate/core' @@ -36,8 +36,6 @@ export interface IndicatorStoreState extends EntityState { dataSources: DataSources currentPage: number - search?: string - locale?: string businessAreas: Record @@ -132,17 +130,18 @@ export class IndicatorsStore extends ComponentStore { readonly currentLang = toSignal(this.#translate.onLangChange.pipe(map((event) => event.lang), startWith(this.#translate.currentLang))) readonly isEmpty = toSignal(this.select((state) => !state.ids.length)) - readonly search = toSignal(this.select((state) => state.search)) - readonly indicators = computed(() => { - const indicators = this.sortedIndicators$() - const text = this.search() + readonly searchText = new BehaviorSubject('') + readonly indicators$ = combineLatest([ + toObservable(this.sortedIndicators$), + this.searchText.pipe(debounceTime(500), map((text) => text?.trim().toLowerCase())) + ]).pipe(map(([indicators, text]) => { if (text) { return indicators.filter( (indicator) => includes(indicator.name.toLowerCase(), text) || includes(indicator.code.toLowerCase(), text) ) } return indicators - }) + })) /** |-------------------------------------------------------------------------- @@ -251,9 +250,9 @@ export class IndicatorsStore extends ComponentStore { }) }) - readonly updateSearch = this.updater((state, text: string) => { - state.search = text - }) + updateSearch(value: string) { + this.searchText.next(value) + } init() { this.setState(adapter.setAll([], initialState)) diff --git a/packages/angular/common/tree-select/tree-select.component.html b/packages/angular/common/tree-select/tree-select.component.html index a8a590f0e..6b22b7031 100644 --- a/packages/angular/common/tree-select/tree-select.component.html +++ b/packages/angular/common/tree-select/tree-select.component.html @@ -1,4 +1,4 @@ -@if (!treeViewer) { +@if (!treeViewer()) { {{ label }} @@ -19,7 +19,10 @@ [formControl]="autoControl" [matChipInputFor]="chipGrid" [matAutocomplete]="auto" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" - (matChipInputTokenEnd)="add($event)"/> + (matChipInputTokenEnd)="add($event)" + (focus)="focus.emit($event)" + (blur)="blur.emit($event)" + /> + @for (conversation of (copilotEnabled() ? conversations() : _mockConversations); track $index; let last = $last) { @if (conversation.command && !conversation.command.hidden) {
/{{conversation.command.name}}
@@ -53,7 +78,7 @@
@if (message.templateRef) { - + } @else { @if (message.content) {
@@ -544,3 +569,31 @@ } + + +
+ + + + + + + + + + + + + + + + + +
Next{{message.data.next}}
Instructions + +
Reasoning{{message.data.reasoning}}
+
+
\ No newline at end of file diff --git a/packages/copilot-angular/src/lib/chat/chat.component.scss b/packages/copilot-angular/src/lib/chat/chat.component.scss index 25e83aedf..5632b4d93 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.scss +++ b/packages/copilot-angular/src/lib/chat/chat.component.scss @@ -65,7 +65,7 @@ @apply rounded-full; } -.mat-mdc-input-element { +.ngm-colpilot__input.mat-mdc-input-element { @apply outline-none bg-transparent; } @@ -126,6 +126,37 @@ bg-transparent hover:bg-neutral-100 dark:hover:bg-neutral-800; } +.ngm-copilot__route-table { + @apply m-2 rounded-lg border border-slate-100 dark:border-slate-700; + + table { + @apply border-collapse; + } + + tr:not(:last-child) { + td { + @apply border-b border-slate-100 dark:border-slate-700; + } + } + + tr { + td { + @apply p-1 pl-2; + } + } + + .ngm-copilot__route-instructions-input { + @apply p-1 border border-transparent rounded-md outline outline-1 outline-offset-1 outline-transparent dark:border-transparent bg-transparent; + + &:hover { + @apply shadow-sm border-slate-200 dark:border-slate-800; + } + &:focus { + @apply shadow-sm border-slate-300 dark:border-slate-800 outline-blue-500; + } + } +} + :host::ng-deep { markdown { > * { diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index 809ee9552..1c1c1fdae 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -10,6 +10,7 @@ import { EventEmitter, Input, Output, + TemplateRef, ViewChild, computed, effect, @@ -48,7 +49,6 @@ import { CopilotChatMessageRoleEnum, CopilotCommand, CopilotContextItem, - CopilotEngine } from '@metad/copilot' import { TranslateModule } from '@ngx-translate/core' import { nanoid } from 'nanoid' @@ -137,9 +137,9 @@ export class NgmCopilotChatComponent { readonly _snackBar = inject(MatSnackBar) private copilotService = inject(NgmCopilotService) - readonly #copilotEngine?: CopilotEngine = inject(NgmCopilotEngineService, { optional: true }) + readonly #copilotEngine?: NgmCopilotEngineService = inject(NgmCopilotEngineService, { optional: true }) - readonly copilotEngine$ = signal(this.#copilotEngine) + readonly copilotEngine$ = signal(this.#copilotEngine) readonly #clipboard: Clipboard = inject(Clipboard) @@ -152,7 +152,7 @@ export class NgmCopilotChatComponent { /** * @deprecated use CopilotRole and Agent instead */ - @Input() get copilotEngine(): CopilotEngine { + @Input() get copilotEngine(): NgmCopilotEngineService { return this.copilotEngine$() } set copilotEngine(value) { @@ -168,6 +168,7 @@ export class NgmCopilotChatComponent { @ViewChild('chatsContent') chatsContent: ElementRef @ViewChild('copilotOptions') copilotOptions: NgxPopperjsContentComponent @ViewChild('scrollBack') scrollBack!: NgmScrollBackComponent + readonly routeTemplate = viewChild('routeTemplate', { read: TemplateRef }) readonly autocompleteTrigger = viewChild('userInput', { read: MatAutocompleteTrigger }) readonly userInput = viewChild('userInput', { read: ElementRef }) @@ -503,6 +504,12 @@ export class NgmCopilotChatComponent { }, { allowSignalWrites: true } ) + + effect(() => { + if (this.copilotEngine) { + this.copilotEngine.routeTemplate = this.routeTemplate() + } + }) } trackByKey(index: number, item) { @@ -825,4 +832,8 @@ export class NgmCopilotChatComponent { async finish(conversation: CopilotChatConversation) { await this.copilotEngine.finish(conversation) } + + onRouteChange(conversationId: string, event: string) { + this.copilotEngine.updateConversationState(conversationId, {instructions: event}) + } } diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index 36280fc16..46b793eef 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -1,10 +1,11 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop' -import { Injectable, computed, inject, signal } from '@angular/core' +import { Injectable, TemplateRef, computed, inject, signal } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages' import { StringOutputParser } from '@langchain/core/output_parsers' import { Runnable } from '@langchain/core/runnables' import { ToolInputParsingException } from '@langchain/core/tools' +import { PregelInputType } from '@langchain/langgraph/dist/pregel' import { BaseCheckpointSaver, END, GraphValueError, StateGraph } from '@langchain/langgraph/web' import { AIOptions, @@ -18,16 +19,16 @@ import { CopilotEngine, DefaultModel, getCommandPrompt, - nanoid, + nanoid } from '@metad/copilot' import { TranslateService } from '@ngx-translate/core' import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents' import { compact, flatten } from 'lodash-es' import { NGXLogger } from 'ngx-logger' import { DropAction, NgmCopilotChatMessage } from '../types' +import { injectCreateChatAgent } from './agent-free' import { NgmCopilotContextToken, recognizeContext, recognizeContextParams } from './context.service' import { NgmCopilotService } from './copilot.service' -import { injectCreateChatAgent } from './agent-free' export const AgentRecursionLimit = 20 @@ -42,6 +43,7 @@ export class NgmCopilotEngineService implements CopilotEngine { readonly checkpointSaver = inject(BaseCheckpointSaver) readonly createChatAgent = injectCreateChatAgent() + public name?: string private api = signal('/api/chat') private chatId = `chat-${uniqueId++}` private key = computed(() => `${this.api()}|${this.chatId}`) @@ -59,6 +61,8 @@ export class NgmCopilotEngineService implements CopilotEngine { readonly llm = toSignal(this.copilot.llm$) readonly secondaryLLM = toSignal(this.copilot.secondaryLLM$) + routeTemplate: TemplateRef | null = null + /** * One conversation including user and assistant messages * This is a array of conversations @@ -272,7 +276,7 @@ export class NgmCopilotEngineService implements CopilotEngine { // Last conversation messages before append new messages const lastConversation = this.lastConversation() - await this.triggerGraphAgent(prompt, lastConversation, freeCommand, { context: this.copilotContext}) + await this.triggerGraphAgent(prompt, lastConversation, freeCommand, { context: this.copilotContext }) // // Allow empty prompt // if (prompt) { @@ -556,7 +560,12 @@ export class NgmCopilotEngineService implements CopilotEngine { } } - async triggerGraphAgent(content: string | null, conversation: CopilotChatConversation, command: CopilotCommand, options?: CopilotChatOptions) { + async triggerGraphAgent( + content: string | null, + conversation: CopilotChatConversation, + command: CopilotCommand, + options?: CopilotChatOptions + ) { // ------------------------- 重复,需重构 const { context, interactive } = options ?? {} @@ -565,8 +574,7 @@ export class NgmCopilotEngineService implements CopilotEngine { // Get chat history messages // const chatHistoryMessages = this.chatHistoryMessages() const lastUserMessages = this.lastUserMessages() - - // const command = conversation.command + let inputState: PregelInputType = null // Context content let contextContent = null @@ -576,14 +584,26 @@ export class NgmCopilotEngineService implements CopilotEngine { return } contextContent = result.contextContent + + const messages = [...lastUserMessages] + if (content) { + messages.push(new HumanMessage({ content })) + } + inputState = { + input: content, + messages, + context: contextContent ? contextContent : null + } } + // Update conversation status to 'answering' this.updateConversation(conversation.id, (conversation) => ({ ...conversation, status: 'answering', abortController })) + // Update last ai message to 'done', and update graph state if message is a route const lastMessage = conversation.messages[conversation.messages.length - 1] if (lastMessage && lastMessage.role === CopilotChatMessageRoleEnum.Assistant && lastMessage.status === 'pending') { this.upsertMessage({ @@ -591,6 +611,8 @@ export class NgmCopilotEngineService implements CopilotEngine { status: 'done' }) } + + // New ai message is thinking const assistantId = nanoid() this.upsertMessage({ id: assistantId, @@ -644,29 +666,24 @@ export class NgmCopilotEngineService implements CopilotEngine { const verbose = this.verbose() try { - const messages = [...lastUserMessages] - if (content) { - messages.push(new HumanMessage({ content })) - } const streamResults = await graph.stream( - content + inputState ? { - input: content, - messages, - context: contextContent ? contextContent : null, + ...inputState, role: this.copilot.rolePrompt(), - language: this.copilot.languagePrompt(), + language: this.copilot.languagePrompt() } : null, { configurable: { - thread_id: this.currentConversationId() + thread_id: conversation.id }, recursionLimit: AgentRecursionLimit } ) - let verboseContent = '' + // let verboseContent = '' + const message = {} as NgmCopilotChatMessage let end = false try { for await (const output of streamResults) { @@ -692,6 +709,12 @@ export class NgmCopilotEngineService implements CopilotEngine { if (value.next === 'FINISH' || value.next === END) { end = true } else { + message.templateRef = this.routeTemplate + message.data = { + next: value.next, + instructions: value.instructions, + reasoning: value.reasoning + } content += `${key}` + '\n\n' + @@ -710,18 +733,18 @@ export class NgmCopilotEngineService implements CopilotEngine { if (content) { if (verbose) { - if (verboseContent) { - verboseContent += '\n\n
' + if (message.content) { + message.content += '\n\n
' } - verboseContent += '✨ ' + content + message.content += '✨ ' + content } else { - verboseContent = content + message.content = content } this.upsertMessage({ + ...message, id: assistantId, role: CopilotChatMessageRoleEnum.Assistant, - content: verboseContent, status: 'thinking' }) } @@ -741,7 +764,7 @@ export class NgmCopilotEngineService implements CopilotEngine { } } - this.updateConversation(this.currentConversationId(), (conversation) => ({ + this.updateConversation(conversation.id, (conversation) => ({ ...conversation, status: end ? 'completed' : 'interrupted' })) @@ -751,7 +774,7 @@ export class NgmCopilotEngineService implements CopilotEngine { this.upsertMessage({ id: assistantId, role: CopilotChatMessageRoleEnum.Assistant, - status: 'done' + status: end ? 'done' : 'pending' }) } else { this.deleteMessage(assistantId) @@ -791,9 +814,9 @@ export class NgmCopilotEngineService implements CopilotEngine { /** * Create graph for command - * - * @param command - * @param interactive + * + * @param command + * @param interactive * @returns CompiledStateGraph */ private async createCommandGraph(command: CopilotCommand, interactive?: boolean) { @@ -888,6 +911,47 @@ export class NgmCopilotEngineService implements CopilotEngine { } } + clear() { + this.conversations$.set([]) + } + + updateConversations(fn: (conversations: Array) => Array): void { + this.conversations$.update(fn) + } + + updateConversation(id: string, fn: (conversation: CopilotChatConversation) => CopilotChatConversation): void { + this.conversations$.update((conversations) => { + const index = conversations.findIndex((conversation) => conversation.id === id) + if (index > -1) { + conversations[index] = fn(conversations[index]) + } + return compact(conversations) + }) + } + + updateLastConversation(fn: (conversations: CopilotChatConversation) => CopilotChatConversation): void { + this.conversations$.update((conversations) => { + const lastIndex = conversations.length - 1 < 0 ? 0 : conversations.length - 1 + const lastConversation = conversations[lastIndex] + conversations[lastIndex] = fn(lastConversation) + return compact(conversations) + }) + } + + updateConversationState(id: string, value: Record) { + const conversation = this.conversations().find((conversation) => conversation.id === id) + if (conversation.graph) { + conversation.graph.updateState( + { + configurable: { + thread_id: id + } + }, + value + ) + } + } + /** * Get message by id from current conversation * @@ -902,7 +966,7 @@ export class NgmCopilotEngineService implements CopilotEngine { * * @param messages */ - upsertMessage(...messages: Partial[]) { + upsertMessage(...messages: Partial[]) { this.conversations$.update((conversations) => { const lastConversation = conversations[conversations.length - 1] const lastMessages = lastConversation.messages @@ -948,33 +1012,6 @@ export class NgmCopilotEngineService implements CopilotEngine { }) } - clear() { - this.conversations$.set([]) - } - - updateConversations(fn: (conversations: Array) => Array): void { - this.conversations$.update(fn) - } - - updateConversation(id: string, fn: (conversation: CopilotChatConversation) => CopilotChatConversation): void { - this.conversations$.update((conversations) => { - const index = conversations.findIndex((conversation) => conversation.id === id) - if (index > -1) { - conversations[index] = fn(conversations[index]) - } - return compact(conversations) - }) - } - - updateLastConversation(fn: (conversations: CopilotChatConversation) => CopilotChatConversation): void { - this.conversations$.update((conversations) => { - const lastIndex = conversations.length - 1 < 0 ? 0 : conversations.length - 1 - const lastConversation = conversations[lastIndex] - conversations[lastIndex] = fn(lastConversation) - return compact(conversations) - }) - } - stopMessage(id: string) { this.updateLastConversation((conversation) => { const message = conversation.messages.find((m) => m.id === id) From 6b7ccce2cb09ddaab038a529193adee71aa6ddce Mon Sep 17 00:00:00 2001 From: meta-d Date: Mon, 22 Jul 2024 17:40:31 +0800 Subject: [PATCH 14/53] feat: modeler agent --- .../project/copilot/indicator/graph.ts | 7 ++- .../model/copilot/cube/cube.command.ts | 3 +- .../model/copilot/cube/graph.ts | 20 ++++--- .../model/copilot/dimension/graph.ts | 5 +- .../model/copilot/modeler/graph.ts | 34 ++++++++++-- .../model/copilot/modeler/planner.ts | 54 +++++++++++-------- .../model/copilot/modeler/supervisor.ts | 9 ++-- .../model/copilot/table/command.ts | 9 ++-- .../model/copilot/table/graph.ts | 7 ++- .../semantic-model/model/model.component.html | 2 +- .../story/copilot/calculation/graph.ts | 46 ++++++++++------ apps/cloud/src/assets/i18n/zh-Hans.json | 3 +- .../src/lib/chat/chat.component.html | 28 ++++++---- .../src/lib/chat/chat.component.scss | 16 +++++- .../copilot-angular/src/lib/i18n/zhHans.ts | 2 +- .../src/lib/services/engine.service.ts | 53 +++++++++--------- packages/copilot/src/lib/graph/team.ts | 6 +-- 17 files changed, 196 insertions(+), 108 deletions(-) diff --git a/apps/cloud/src/app/features/project/copilot/indicator/graph.ts b/apps/cloud/src/app/features/project/copilot/indicator/graph.ts index e27949e93..1392f68dd 100644 --- a/apps/cloud/src/app/features/project/copilot/indicator/graph.ts +++ b/apps/cloud/src/app/features/project/copilot/indicator/graph.ts @@ -36,7 +36,12 @@ export function injectCreateIndicatorGraph() { const tags = projectService.tags return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { - const supervisorNode = await Team.createSupervisor(llm, [INDICATOR_AGENT_NAME]) + const supervisorNode = await Team.createSupervisor(llm, [ + { + name: INDICATOR_AGENT_NAME, + description: 'The agent will create indicator, only one at a time' + } + ]) const createIndicator = await createIndicatorWorker( { diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/cube/cube.command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/cube/cube.command.ts index 11d229949..c32a6846a 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/cube/cube.command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/cube/cube.command.ts @@ -49,7 +49,8 @@ export function injectCubeCommand(dimensions: Signal) { }, agent: { type: CopilotAgentType.Graph, - conversation: true + conversation: true, + interruptBefore: ['tools'] }, fewShotPrompt, createGraph: createCube diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/cube/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/cube/graph.ts index 20455dc51..a9506657a 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/cube/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/cube/graph.ts @@ -28,8 +28,9 @@ export function injectCubeModeler() { const systemContext = async () => { const sharedDimensions = dimensions().filter((dimension) => dimension.hierarchies?.length) return ( - `{role}\n` + - ` The cube name can't be the same as the fact table name.` + +`{role} +{language} +The cube name can't be the same as the fact table name.` + (cubes().length ? ` The cube name cannot be any of the following existing cubes [${cubes() .map(({ name }) => name) @@ -46,17 +47,19 @@ ${markdownSharedDimensions(sharedDimensions)} ) } - return async ({ llm, checkpointer }: CreateGraphOptions) => { + return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { const state: StateGraphArgs['channels'] = createCopilotAgentState() return createReactAgent({ llm, checkpointSaver: checkpointer, state, + interruptBefore, + interruptAfter, tools: [selectTablesTool, queryTablesTool, createCubeTool], messageModifier: async (state) => { const system = await SystemMessagePromptTemplate.fromTemplate( SYSTEM_PROMPT + `\n\n${await systemContext()}\n\n` + `{context}` - ).format(state as any) + ).format(state) return [new SystemMessage(system), ...state.messages] } }) @@ -67,16 +70,17 @@ export function injectRunCubeModeler() { const createCubeModeler = injectCubeModeler() const fewShotPrompt = injectAgentFewShotTemplate(CubeCommandName, { k: 1, vectorStore: null }) - return async ({ llm, checkpointer }: CreateGraphOptions) => { - const agent = await createCubeModeler({ llm, checkpointer }) + return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { + const agent = await createCubeModeler({ llm, checkpointer, interruptBefore, interruptAfter }) return RunnableLambda.from(async (state: AgentState) => { - const content = await fewShotPrompt.format({ input: state.input, context: state.context }) + const content = await fewShotPrompt.format({ input: state.input, context: '' }) return { input: state.input, messages: [new HumanMessage(content)], role: state.role, - context: state.context + context: state.context, + language: state.language } }) .pipe(agent) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/graph.ts index e8632cad4..9d000dd50 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/graph.ts @@ -65,12 +65,13 @@ export function injectRunDimensionModeler() { const agent = await createDimensionModeler({ llm, checkpointer }) return RunnableLambda.from(async (state: AgentState) => { - const content = await fewShotPrompt.format({ input: state.input, context: state.context }) + const content = await fewShotPrompt.format({ input: state.input, context: '' }) return { input: state.input, messages: [new HumanMessage(content)], role: state.role, - context: state.context + context: state.context, + language: state.language } }) .pipe(agent) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts index a743c1e67..e2eb41f12 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts @@ -6,6 +6,7 @@ import { DIMENSION_MODELER_NAME, injectRunDimensionModeler } from '../dimension/ import { injectRunModelerPlanner } from './planner' import { createSupervisor } from './supervisor' import { PLANNER_NAME, SUPERVISOR_NAME, State } from './types' +import { HumanMessage } from '@langchain/core/messages' const superState: StateGraphArgs['channels'] = { ...Team.createState(), @@ -21,11 +22,33 @@ export function injectCreateModelerGraph() { const createCubeModeler = injectRunCubeModeler() return async ({ llm }: CreateGraphOptions) => { - const supervisorNode = await createSupervisor(llm, [PLANNER_NAME, DIMENSION_MODELER_NAME, CUBE_MODELER_NAME]) + const supervisorNode = await createSupervisor(llm, [ + { + name: PLANNER_NAME, + description: 'Create a plan for modeling' + }, + { + name: DIMENSION_MODELER_NAME, + description: 'Create a dimension, only one at a time' + }, + { + name: CUBE_MODELER_NAME, + description: 'Create a cube, only one at a time' + } + ]) const plannerAgent = await createModelerPlanner({ llm }) const superGraph = new StateGraph({ channels: superState }) // Add steps nodes + .addNode(SUPERVISOR_NAME, RunnableLambda.from(async (state: State) => { + const _state = await supervisorNode.invoke(state) + return { + ..._state, + messages: [ + new HumanMessage(`Call ${_state.next} with instructions: ${_state.instructions}`) + ] + } + })) .addNode(PLANNER_NAME, RunnableLambda.from(async (state: State) => { return plannerAgent.invoke({ @@ -43,7 +66,9 @@ export function injectCreateModelerGraph() { return { input: state.instructions, role: state.role, - context: state.context + context: state.context, + language: state.language, + messages: [] } }).pipe(await createDimensionModeler({ llm })) ) @@ -53,11 +78,12 @@ export function injectCreateModelerGraph() { return { input: state.instructions, role: state.role, - context: state.context + context: state.context, + language: state.language, + messages: [] } }).pipe(await createCubeModeler({ llm })) ) - .addNode(SUPERVISOR_NAME, supervisorNode) superGraph.addEdge(PLANNER_NAME, SUPERVISOR_NAME) superGraph.addEdge(DIMENSION_MODELER_NAME, SUPERVISOR_NAME) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts index 139915208..f75c853c4 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts @@ -13,24 +13,28 @@ import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' import { createCopilotAgentState, createReactAgent, Team } from '@metad/copilot' const SYSTEM_PROMPT = - `You are a cube modeler for data analysis, now you need create a plan for the final goal.` + - `\n{role}\n` + - `\n1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes.` + - ` Or use the 'selectTables' tool to get all tables then select the required physical tables from them. And use 'queryTables' tool get columns metadata for the required tables.` + - `\n2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions.` + - ` If the dimension required for modeling is in the following existing shared dimensions, please do not put it in the plan, just use it directly in the cube creation task.` + - ` Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension.` + - ` For example, create a dimension called Department with the fields: First-level Department, Second-level Department, Third-level Department, and add First-level Department, Second-level Department, and Third-level Department as the levels of the dimension in order.` + - `\nIf you are creating a shared dimension, please provide your reason.` + - `\n3. Distinguish whether each table is a dimension table or a fact table. If it is a dimension table, you need to create a shared dimension for it. If it is a fact table, create a cube and inline dimension or associate the created shared dimension.` + - `\n4. Each step of the plan corresponds to one of the tasks 'Create a shared dimension' and 'Create a cube with inline dimensions and share dimensions'. ` + - `\n\nA plan is an array of independent, ordered steps.` + - ' This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps.' + - ' The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n\n' + - `For example: \n` + - `Objective is: Create a cube using the table.` + - `\nTable context:` + - `Table is: + `You are a cube modeler for data analysis, now you need create a plan for the final goal. +{role} +{language} +1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes. + Or use the 'selectTables' tool to get all tables then select the required physical tables from them. +2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. + If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. + If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions. + Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension. + For example, create a dimension called Department with the fields: First-level Department, Second-level Department, Third-level Department, and add First-level Department, Second-level Department, and Third-level Department as the levels of the dimension in order. + If you are creating a shared dimension, please provide your reason. +3. Distinguish whether each table is a dimension table or a fact table. If it is a dimension table, you need to create a shared dimension for it. If it is a fact table, create a cube and inline dimension or associate the created shared dimension. +4. Each step of the plan corresponds to one of the tasks 'Create a shared dimension' and 'Create a cube with inline dimensions and share dimensions'. + +A plan is an array of independent, ordered steps. +This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. +The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps. + +For example: + Objective is: Create a cube using the table. + Table context: + Table is: - name: sales_data caption: 销售数据 columns: @@ -62,9 +66,14 @@ The product_code field has a dimension table 'product_data', so it is a shared d The plan are as follows: 1. Create a shared dimension 'Product' for table product_data. 2. Create a cube with share dimension 'Product' and inline dimensions: 'Company' for table sales_data. -` + - `\nTable context:\n{context}` + - `\n{dimensions}` + +Table context: +{context} + +Avoid creating already existing shared dimensions: +{dimensions} +just use them directly in the cube creation task. +` export function injectCreateModelerPlanner() { const modelService = inject(SemanticModelService) @@ -114,7 +123,8 @@ export function injectRunModelerPlanner() { input: state.input, messages: [new HumanMessage(content)], role: state.role, - context: state.context + context: state.context, + language: state.language, } }) .pipe(agent) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts index 807c7077b..c0eecd350 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts @@ -1,13 +1,14 @@ import { ChatOpenAI } from '@langchain/openai' import { Team } from '@metad/copilot' -export async function createSupervisor(llm: ChatOpenAI, members: string[]) { +export async function createSupervisor(llm: ChatOpenAI, members: {name: string; description: string;}[]) { const supervisorNode = await Team.createSupervisor( llm, members, - 'You are a supervisor tasked with managing a conversation between the' + - ' following teams: {team_members}. Given the following user request,' + - // `call the ${PLANNER_NAME} to get a plan steps firstly.` + + `You are a supervisor tasked with managing a conversation between the following teams: +{team_members} + +Given the following user request ` + `Create a plan then execute the plan steps one by one,` + ' by respond with the worker to act next. Each worker will perform a' + ' step task and respond with their results and status. When finished,' + diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts index 365e8e8df..18966e225 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts @@ -1,11 +1,11 @@ -import { Signal, inject } from '@angular/core' +import { inject } from '@angular/core' import { CopilotAgentType } from '@metad/copilot' import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' +import { injectAgentFewShotTemplate } from '../../../../../@core/copilot' import { SemanticModelService } from '../../model.service' import { injectTableCreator } from './graph' -import { injectAgentFewShotTemplate } from '../../../../../@core/copilot' export function injectTableCommand() { const logger = inject(NGXLogger) @@ -14,7 +14,7 @@ export function injectTableCommand() { const createTableCreator = injectTableCreator() const commandName = 'table' - const fewShotPrompt = injectAgentFewShotTemplate(commandName, {k: 1, vectorStore: null}) + const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) return injectCopilotCommand(commandName, { alias: 't', description: translate.instant('PAC.MODEL.Copilot.CommandTableDesc', { @@ -28,7 +28,8 @@ export function injectTableCommand() { }, agent: { type: CopilotAgentType.Graph, - conversation: true + conversation: true, + interruptBefore: ['tools'] }, fewShotPrompt, createGraph: createTableCreator diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts index 6ec64a55c..b7f4ed369 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts @@ -13,6 +13,7 @@ function createSystemPrompt(dialect: string) { return ( `You are a cube modeling expert. Let's create or edit the pyhsical table! {role} +{language} The database dialect is '${dialect}'. You need add short label to the created table and it's columns. {context}` @@ -25,13 +26,15 @@ export function injectTableCreator() { const dialect = modelService.dialect - return async ({ llm, checkpointer }: CreateGraphOptions) => { + return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { const state: StateGraphArgs['channels'] = createCopilotAgentState() return createReactAgent({ llm, checkpointSaver: checkpointer, state, - tools: [createTableTool], + interruptBefore, + interruptAfter, + tools: [ createTableTool ], messageModifier: async (state) => { const system = await SystemMessagePromptTemplate.fromTemplate(createSystemPrompt(dialect())).format(state) return [new SystemMessage(system), ...state.messages] diff --git a/apps/cloud/src/app/features/semantic-model/model/model.component.html b/apps/cloud/src/app/features/semantic-model/model/model.component.html index 6a9c7e8ce..3f5407e26 100644 --- a/apps/cloud/src/app/features/semantic-model/model/model.component.html +++ b/apps/cloud/src/app/features/semantic-model/model/model.component.html @@ -35,7 +35,7 @@
diff --git a/apps/cloud/src/app/features/story/copilot/calculation/graph.ts b/apps/cloud/src/app/features/story/copilot/calculation/graph.ts index fa23e0ca8..fb452a76a 100644 --- a/apps/cloud/src/app/features/story/copilot/calculation/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/calculation/graph.ts @@ -67,23 +67,37 @@ export async function createCalculationGraph({ const supervisorNode = await Team.createSupervisor( llm, [ - RESTRICTED_AGENT_NAME, - CONDITIONAL_AGGREGATION_AGENT_NAME, - VARIANCE_AGENT_NAME, - FORMULA_AGENT_NAME, - MEASURE_CONTROL_AGENT_NAME + { + name: RESTRICTED_AGENT_NAME, + description: 'This agent allows the creation of measures that aggregate values based on restrictions imposed by dimension members. It is useful when you need to filter or limit the data aggregation to specific members of a dimension.' + }, + { + name: CONDITIONAL_AGGREGATION_AGENT_NAME, + description: 'This agent provides the ability to create aggregated measures based on various operations and dimensions. It supports operations such as Count, Sum, TopCount, TopSum, Min, Max, and Avg. This function is suitable when you need to perform different types of aggregations based on certain conditions.' + }, + { + name: VARIANCE_AGENT_NAME, + description: 'This agent is designed to create measures that calculate the variance or ratio between different members within a dimension. It is useful for comparing data, such as year-over-year changes, month-over-month changes, differences between versions, or differences between accounts.' + }, + { + name: FORMULA_AGENT_NAME, + description: 'When none of the above agents can meet the requirements, this agent allows the creation of calculated measures using MDX (Multidimensional Expressions) formulas. It provides the flexibility to define complex calculations and custom aggregations.' + }, + { + name: MEASURE_CONTROL_AGENT_NAME, + description: 'This agent create a calculation measure that allows the selection among multiple measures. It is useful when you need to provide users with the ability to choose from different measures dynamically.' + } ], - `You are a supervisor responsible for selecting one of the following workers: {team_members} .` + - ` Here are the explanations of these workers to help the agent select the appropriate tools for creating a calculation measure in Cube:` + - `\n1. **RestrictedMeasureAgent**: This agent allows the creation of measures that aggregate values based on restrictions imposed by dimension members. It is useful when you need to filter or limit the data aggregation to specific members of a dimension.` + - `\n2. **ConditionalAggregationAgent**: This agent provides the ability to create aggregated measures based on various operations and dimensions. It supports operations such as Count, Sum, TopCount, TopSum, Min, Max, and Avg. This function is suitable when you need to perform different types of aggregations based on certain conditions.` + - `\n3. **VarianceMeasureAgent**: This agent is designed to create measures that calculate the variance or ratio between different members within a dimension. It is useful for comparing data, such as year-over-year changes, month-over-month changes, differences between versions, or differences between accounts.` + - `\n4. **FormulaMeasureAgent**: When none of the above agents can meet the requirements, this agent allows the creation of calculated measures using MDX (Multidimensional Expressions) formulas. It provides the flexibility to define complex calculations and custom aggregations.` + - `\n5. **MeasureControlAgent**: This agent create a calculation measure that allows the selection among multiple measures. It is useful when you need to provide users with the ability to choose from different measures dynamically.` + - ` Based on the following user request, select an appropriate worker to create a calculation measure.` + - ` When finished, respond FINISH. Choose strategically to minimize the number of steps taken.` + - `\n\n{role}` + - `\n\n{context}` + `You are a supervisor responsible for selecting one of the following workers: +{team_members} + +Based on the following user request, select an appropriate worker to create a calculation measure. +When finished, respond FINISH. Choose strategically to minimize the number of steps taken. + +{role} + +{context}` + ) const createFormulaAgent = await createFormulaWorker({ diff --git a/apps/cloud/src/assets/i18n/zh-Hans.json b/apps/cloud/src/assets/i18n/zh-Hans.json index 0e7f5d5aa..8f5f1b9c0 100644 --- a/apps/cloud/src/assets/i18n/zh-Hans.json +++ b/apps/cloud/src/assets/i18n/zh-Hans.json @@ -662,7 +662,7 @@ "SyncMembersJobRunning": "同步成员任务正在运行", "QUERY": { - "TITLE": "查询", + "TITLE": "查询实验室", "SaveAsModel": "另存为模型", "SaveAsDBInit": "另存为数据库初始化脚本", "TableSchema": "表结构", @@ -1717,7 +1717,6 @@ } }, "WIDGETS": { - "FilterBar": { "Query": "查询" }, diff --git a/packages/copilot-angular/src/lib/chat/chat.component.html b/packages/copilot-angular/src/lib/chat/chat.component.html index 08b8a7cb9..6e45c13a1 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.html +++ b/packages/copilot-angular/src/lib/chat/chat.component.html @@ -145,12 +145,13 @@ }
} - @if (message.error) { -
- 🙈 - {{ message.error }} -
- } + } + + @if (message.error) { +
+ 🙈 + {{ message.error }} +
} diff --git a/apps/cloud/src/app/features/semantic-model/model/entity/cube-structure/cube-structure.component.ts b/apps/cloud/src/app/features/semantic-model/model/entity/cube-structure/cube-structure.component.ts index 4805b4fa2..cb9e5a8c7 100644 --- a/apps/cloud/src/app/features/semantic-model/model/entity/cube-structure/cube-structure.component.ts +++ b/apps/cloud/src/app/features/semantic-model/model/entity/cube-structure/cube-structure.component.ts @@ -8,8 +8,10 @@ import { Output, ViewChildren, booleanAttribute, + computed, inject, - input + input, + model } from '@angular/core' import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop' import { FormsModule } from '@angular/forms' @@ -34,7 +36,7 @@ import { TranslateModule } from '@ngx-translate/core' import { uuid } from 'apps/cloud/src/app/@core' import { MaterialModule } from 'apps/cloud/src/app/@shared' import { NGXLogger } from 'ngx-logger' -import { filter, map, switchMap, withLatestFrom } from 'rxjs' +import { combineLatest, combineLatestWith, filter, map, switchMap, withLatestFrom } from 'rxjs' import { SemanticModelService } from '../../model.service' import { CdkDragDropContainers, @@ -94,28 +96,31 @@ export class ModelCubeStructureComponent { @Output() editChange = new EventEmitter() @ViewChildren(CdkDropList) cdkDropList: CdkDropList[] + + /** + |-------------------------------------------------------------------------- + | Signals + |-------------------------------------------------------------------------- + */ + readonly search = model('') + readonly dimensions = computed(() => { + const dimensions = this.entityService.dimensions() + const search = this.search() + if (search) { + const text = search.trim().toLowerCase() + return dimensions?.filter(({ name, caption }) => name.toLowerCase().includes(text) || caption?.toLowerCase().includes(text)) + } + return dimensions + }) - public readonly dimensionUsages$ = this.entityService.dimensionUsages$.pipe( - withLatestFrom(this.modelService.sharedDimensions$), - map(([dimensionUsages, sharedDimensions]) => { - return dimensionUsages?.map((usage) => { - const dimension = sharedDimensions.find((item) => usage.source === item.name) - return { - usage, - dimension: { - ...(dimension ?? {}), - name: usage.name, - caption: usage.caption || dimension?.caption, - __id__: usage.__id__ - } + readonly calculatedMembers = toSignal( + combineLatest([this.entityService.calculatedMembers$, toObservable(this.search)]) + .pipe( + map(([members, search]) => { + if (search) { + const text = search.trim().toLowerCase() + members = members?.filter(({ name, caption }) => name.toLowerCase().includes(text) || caption?.toLowerCase().includes(text)) } - }) - }) - ) - - public readonly calculatedMembers = toSignal( - this.entityService.calculatedMembers$.pipe( - map((members) => { return members?.map( (member) => ({ @@ -127,15 +132,48 @@ export class ModelCubeStructureComponent { }) ) ) + + readonly measures = computed(() => { + const measures = this.entityService.measures() + const search = this.search() + if (search) { + const text = search.trim().toLowerCase() + return measures?.filter(({ name, caption }) => name.toLowerCase().includes(text) || caption?.toLowerCase().includes(text)) + } + return measures + }) + readonly selectedProperty = this.entityService.selectedProperty + readonly entityType = toSignal(this.entityService.originalEntityType$) + + /** |-------------------------------------------------------------------------- - | Signals + | Observables |-------------------------------------------------------------------------- */ - readonly measures = this.entityService.measures - readonly selectedProperty = this.entityService.selectedProperty - readonly entityType = toSignal(this.entityService.originalEntityType$) + readonly dimensionUsages$ = this.entityService.dimensionUsages$.pipe( + withLatestFrom(this.modelService.sharedDimensions$), + combineLatestWith(toObservable(this.search)), + map(([[dimensionUsages, sharedDimensions], search]) => { + if (search) { + search = search.trim().toLowerCase() + dimensionUsages = dimensionUsages?.filter((usage) => usage.name.toLowerCase().includes(search) || usage.caption?.toLowerCase().includes(search)) + } + return dimensionUsages?.map((usage) => { + const dimension = sharedDimensions.find((item) => usage.source === item.name) + return { + usage, + dimension: { + ...(dimension ?? {}), + name: usage.name, + caption: usage.caption || dimension?.caption, + __id__: usage.__id__ + } + } + }) + }) + ) /** |-------------------------------------------------------------------------- @@ -156,14 +194,17 @@ export class ModelCubeStructureComponent { __id__: uuid(), name: dimension.name, caption: dimension.caption, + visible: dimension.visible ?? true, hierarchies: dimension.hierarchies?.map((hierarchy) => ({ __id__: uuid(), name: hierarchy.name, caption: hierarchy.caption, + visible: hierarchy.visible ?? true, levels: hierarchy.levels?.map((level) => ({ __id__: uuid(), name: level.name, - caption: level.caption + caption: level.caption, + visible: level.visible ?? true, })) })) })) @@ -175,7 +216,8 @@ export class ModelCubeStructureComponent { measures: getEntityMeasures(entityType).map((measure) => ({ __id__: uuid(), name: measure.name, - caption: measure.caption + caption: measure.caption, + visible: measure.visible ?? true, })) }) } diff --git a/apps/cloud/src/app/features/semantic-model/model/schema/cube-attributes.schema.ts b/apps/cloud/src/app/features/semantic-model/model/schema/cube-attributes.schema.ts index db1e50e5d..39751d7c0 100644 --- a/apps/cloud/src/app/features/semantic-model/model/schema/cube-attributes.schema.ts +++ b/apps/cloud/src/app/features/semantic-model/model/schema/cube-attributes.schema.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import { Cube } from '@metad/ocap-core' -import { FORMLY_ROW, FORMLY_W_1_2 } from '@metad/story/designer' +import { FORMLY_ROW, FORMLY_W_1_2, FORMLY_W_FULL } from '@metad/story/designer' import { filter, map, switchMap } from 'rxjs' import { EntitySchemaService } from './entity-schema.service' import { CubeSchemaState } from './types' @@ -42,7 +42,10 @@ export class CubeAttributesSchema extends EntitySchemaService extends EntitySchemaService extends CubeSchemaService { - public readonly hierarchies$ = this.select((state) => { + readonly hierarchies$ = this.select((state) => { return state.cube.dimensions.find((item) => item.__id__ === state.modeling.__id__)?.hierarchies }) @@ -31,7 +31,7 @@ export class DimensionAttributesSchema { diff --git a/libs/core-angular/src/lib/copilot/utils.ts b/libs/core-angular/src/lib/copilot/utils.ts index 894a9b98f..3936349c2 100644 --- a/libs/core-angular/src/lib/copilot/utils.ts +++ b/libs/core-angular/src/lib/copilot/utils.ts @@ -1,4 +1,4 @@ -import { Cube, EntityType, getEntityDimensions, getEntityMeasures, getEntityVariables } from '@metad/ocap-core' +import { Cube, EntityType, getDimensionHierarchies, getEntityDimensions, getEntityMeasures, getEntityVariables, getHierarchyLevels } from '@metad/ocap-core' import { nonBlank } from '../helpers' /** @@ -29,33 +29,44 @@ export function calcEntityTypePrompt(entityType: EntityType) { export function markdownEntityType(entityType: EntityType) { const variables = getEntityVariables(entityType) - return `The cube definition for ${entityType.name} is as follows: -name: "${entityType.name}" -caption: "${entityType.caption || ''}" + return `The cube definition for (${entityType.name}) is as follows: +name: ${entityType.name} +caption: ${entityType.caption || ''} +description: > +${prepend(' ', entityType.description || entityType.caption)} dimensions: ${getEntityDimensions(entityType) .map((dimension) => [ ` - name: "${dimension.name}"`, ` caption: "${dimension.caption || ''}"`, + dimension.description ? +` description: > +${prepend(' ', dimension.description)}` : null, dimension.semantics?.semantic ? ` semantic: ${dimension.semantics.semantic}` : null, ` hierarchies:` ].filter(nonBlank).join('\n') + '\n' + -dimension.hierarchies?.map((item) => -` - name: "${item.name}" - caption: "${item.caption || ''}" - levels: -${item.levels?.map((item) => +getDimensionHierarchies(dimension).map((item) =>[ +` - name: "${item.name}"`, +` caption: "${item.caption || ''}"`, +item.description ? +` description: > +${prepend(' ', item.description)}` : null, +` levels: +${getHierarchyLevels(item).map((item) => [ ` - name: "${item.name}"`, ` caption: "${item.caption || ''}"`, +item.description ? +` description: > +${prepend(' ', item.description)}` : null, item.semantics?.semantic ? ` semantic: ${item.semantics.semantic}` : null, item.semantics?.formatter ? ` time_formatter: "${item.semantics.formatter}"` : null, ].filter(nonBlank).join('\n')).join('\n')} -`).join('\n') ?? '' +`].join('\n')).join('\n') ?? '' ).join('\n')} measures: ${getEntityMeasures(entityType).map((item) => @@ -63,8 +74,8 @@ ${getEntityMeasures(entityType).map((item) => ` - name: "${item.name}"`, ` caption: ${item.caption || ''}`, item.description ? - ` description: > ${item.description}` : null - + ` description: > +${prepend(' ', item.description)}` : null ].filter(nonBlank).join(`\n`) ).join('\n')} ` + (variables.length ? @@ -162,4 +173,8 @@ export function markdownTable(table: EntityType) { .join('\n'), '```' ].join('\n') +} + +export function prepend(prefix: string, text: string) { + return text.split('\n').map(line => prefix + line).join('\n') } \ No newline at end of file diff --git a/libs/story-angular/designer/schemas/data-settings.schema.ts b/libs/story-angular/designer/schemas/data-settings.schema.ts index 5543d2ff9..c0729e2d5 100644 --- a/libs/story-angular/designer/schemas/data-settings.schema.ts +++ b/libs/story-angular/designer/schemas/data-settings.schema.ts @@ -11,8 +11,10 @@ import { DataSource, EntityType, Indicator, + MDCube, getEntityDimensions, - getEntityMeasures + getEntityMeasures, + isVisible } from '@metad/ocap-core' import { NxStoryService } from '@metad/story/core' import { FormlyFieldConfig } from '@ngx-formly/core' @@ -283,23 +285,20 @@ export abstract class DataSettingsSchemaService< field.props.error.set(null) }), switchMap((dataSource: DataSource) => - combineLatest([ dataSource.discoverMDCubes().pipe( catchError((err) => { field.props.error.set(err.message) return of([]) }) - ), - dataSource.selectSchema() - ]) + ) ), - map(([cubes, schema]) => { - return cubes.map((cube: any) => ({ + map((cubes) => { + return cubes.filter(isVisible).map((cube: MDCube) => ({ value: cube, key: cube.name, caption: cube.caption, // @todo - icon: schema?.cubes?.find((item) => item.name === cube.name) + icon: cube.annotated ? 'star_outline' : cube.cubeType === 'VIRTUAL CUBE' ? 'dataset_linked' @@ -307,10 +306,6 @@ export abstract class DataSettingsSchemaService< fontSet: 'material-icons-outlined' })) }), - // catchError((err) => { - // field.className = field.className.split('formly-loader').join('') - // return throwError(() => err) - // }), tap(() => (field.className = field.className.split('formly-loader').join(''))) ) } diff --git a/packages/angular/entity/property-select/property-select.component.ts b/packages/angular/entity/property-select/property-select.component.ts index 19cae7c36..4a6d4ce52 100644 --- a/packages/angular/entity/property-select/property-select.component.ts +++ b/packages/angular/entity/property-select/property-select.component.ts @@ -292,7 +292,7 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi readonly property = toSignal(this.property$) readonly hierarchies$: Observable> = this.property$.pipe( - map((dimension) => dimension?.hierarchies), + map((dimension) => dimension?.hierarchies?.filter(isVisible)), shareReplay(1) ) @@ -305,7 +305,7 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi ) readonly levels$ = this.hierarchy$.pipe( - map((hierarchy: PropertyHierarchy) => hierarchy?.levels), + map((hierarchy: PropertyHierarchy) => hierarchy?.levels?.filter(isVisible)), shareReplay(1) ) diff --git a/packages/core/src/lib/models/helper.ts b/packages/core/src/lib/models/helper.ts index f219a876e..c79c7d6b1 100644 --- a/packages/core/src/lib/models/helper.ts +++ b/packages/core/src/lib/models/helper.ts @@ -1,5 +1,15 @@ import { Semantics } from '../annotations' -import { Dimension, getPropertyHierarchy, getPropertyName, IMember, isDimension, ISlicer, isMeasure, Measure, Member } from '../types' +import { + Dimension, + getPropertyHierarchy, + getPropertyName, + IMember, + isDimension, + ISlicer, + isMeasure, + Measure, + Member +} from '../types' import { assignDeepOmitBlank, isEmpty, isNil, isString, omit, omitBy } from '../utils' import { CalculationProperty, @@ -25,7 +35,6 @@ import { VariableProperty } from './sdl' - export function serializeUniqueName(dimension: string, hierarchy?: string, level?: string, intrinsic?: string) { const name = !!hierarchy && dimension !== hierarchy ? `[${dimension}.${hierarchy}]` : `[${dimension}]` if (intrinsic) { @@ -102,6 +111,12 @@ export function getEntityProperty2 item.role === AggregationRole.hierarchy) -} +// export function getEntityHierarchies(entityType: EntityType): Property[] { +// if (isNil(entityType?.properties)) { +// return [] +// } +// return Object.values(entityType?.properties).filter((item) => item.role === AggregationRole.hierarchy) +// } export function getEntityDimensionAndHierarchies(entityType: EntityType): Property[] { - const results = [] if (isNil(entityType?.properties)) { return [] } + const results = [] Object.values(entityType.properties).forEach((property) => { if (property.role === AggregationRole.dimension) { results.push(property) @@ -137,7 +152,8 @@ export function getEntityDimensionAndHierarchies(entityType: EntityType): Proper } /** - * 返回 EntityType 中的度量字段列表 + * Get measures in entity type, filtered by is visible or empty + * * @param entityType * @returns */ @@ -146,18 +162,50 @@ export function getEntityMeasures(entityType: EntityType): PropertyMeasure[] { return [] } return Object.values(entityType.properties).filter( - (property) => property.role === AggregationRole.measure && (isNil(property.visible) || property.visible) + (property) => property.role === AggregationRole.measure && isVisible(property) ) } +/** + * Get all indicator type measures in entity type, filtered by is visible or empty + * + * @param entityType + * @returns + */ export function getEntityIndicators(entityType: EntityType): RestrictedMeasureProperty[] { return getEntityMeasures(entityType).filter(isIndicatorMeasureProperty) } +/** + * Get default measure in entity type or first measure property + * + * @param entityType + * @returns + */ export function getEntityDefaultMeasure(entityType: EntityType) { return entityType.defaultMeasure ? entityType.properties[entityType.defaultMeasure] : getEntityMeasures(entityType)[0] } +/** + * Get visiable hierarchies from dimension property + * + * @param dimension + * @returns + */ +export function getDimensionHierarchies(dimension: PropertyDimension) { + return dimension?.hierarchies?.filter(isVisible) ?? [] +} + +/** + * Get visiable levels from hierarchy property + * + * @param hierarchy + * @returns + */ +export function getHierarchyLevels(hierarchy: PropertyHierarchy) { + return hierarchy?.levels?.filter(isVisible) ?? [] +} + /** * Get hierarchy proeprty from EntityType: * @@ -218,7 +266,9 @@ export function getEntityParameters(entityType: EntityType): ParameterProperty[] } export function getEntityVariables(entityType: EntityType): VariableProperty[] { - return getEntityParameters(entityType).filter((parameter) => parameter.role === AggregationRole.variable) as VariableProperty[] + return getEntityParameters(entityType).filter( + (parameter) => parameter.role === AggregationRole.variable + ) as VariableProperty[] } /** @@ -273,11 +323,11 @@ export function getPropertyUnitName(property: Property) { * @deprecated use {@link getMemberKey} */ export function getMemberValue(member: Member): string { - return isString(member) ? member : (member?.key || member?.value as string) + return isString(member) ? member : member?.key || (member?.value as string) } export function getMemberKey(member: Member): string { - return isString(member) ? member : (member?.key || member?.value as string) + return isString(member) ? member : member?.key || (member?.value as string) } export function hasLevel(dimension: Dimension | string) { @@ -650,7 +700,7 @@ export function getMemberFromRow(row: unknown, dimension: Dimension, entityType? key: row[dimension.hierarchy || dimension.dimension], value: row[dimension.hierarchy || dimension.dimension], label: caption ? row[caption] : null, - caption: caption ? row[caption] : null, + caption: caption ? row[caption] : null } } diff --git a/packages/core/src/lib/models/sdl.ts b/packages/core/src/lib/models/sdl.ts index 263100f37..c87d48c18 100644 --- a/packages/core/src/lib/models/sdl.ts +++ b/packages/core/src/lib/models/sdl.ts @@ -24,6 +24,10 @@ export interface Entity { * Visible Property */ visible?: boolean + /** + * Long text description of entity + */ + description?: string } export interface Schema { diff --git a/packages/xmla/src/lib/ds-xmla.service.ts b/packages/xmla/src/lib/ds-xmla.service.ts index f7bb9b884..7b929b020 100644 --- a/packages/xmla/src/lib/ds-xmla.service.ts +++ b/packages/xmla/src/lib/ds-xmla.service.ts @@ -164,7 +164,7 @@ export class XmlaDataSource extends AbstractDataSource { return this.selectEntitySets(refresh) as unknown as Observable } /** - * Observable of cubes in xmla source, then merge 'caption' from custom schema + * Observable of cubes in xmla source, then merge caption and description from custom schema * * @param refresh For refresh cache * @returns @@ -180,6 +180,8 @@ export class XmlaDataSource extends AbstractDataSource { const index = cubes.findIndex((cube) => item.name === cube.name) if (index > -1) { cubes[index].caption = item.caption + cubes[index].description = item.description + cubes[index].visible = item.visible cubes[index].annotated = true results.push(...cubes.splice(index, 1)) } @@ -989,7 +991,8 @@ export class XmlaDataSource extends AbstractDataSource { return { ...rtEntityType, - caption: rtEntityType.caption || cube.caption, + caption: cube.caption || rtEntityType.caption, + description: cube.description || rtEntityType.description, properties } } From cf9caed0cf917afced1c5cc74c2377a035c0ef24 Mon Sep 17 00:00:00 2001 From: meta-d Date: Tue, 23 Jul 2024 22:06:17 +0800 Subject: [PATCH 16/53] feat: property select optimization --- .../property-select.component.html | 28 +++++-- .../property-select.component.scss | 7 ++ .../property-select.component.ts | 81 +++++++++++++++---- packages/core/src/lib/models/helper.ts | 6 +- packages/core/src/lib/models/sdl.ts | 36 ++++++--- packages/xmla/src/lib/ds-xmla.service.ts | 12 +-- packages/xmla/src/lib/mdx-query.ts | 5 +- packages/xmla/src/lib/types/metadata.ts | 3 +- packages/xmla/src/lib/types/rowset.ts | 22 +---- 9 files changed, 133 insertions(+), 67 deletions(-) diff --git a/packages/angular/entity/property-select/property-select.component.html b/packages/angular/entity/property-select/property-select.component.html index 26545c582..a6ae753f7 100644 --- a/packages/angular/entity/property-select/property-select.component.html +++ b/packages/angular/entity/property-select/property-select.component.html @@ -14,7 +14,7 @@
- @@ -56,15 +56,26 @@ - - - - - + (click)="$event.stopPropagation()"/> + + @if (showDimension) { + + + + + + + } - + @if (showMeasure) { + @for (property of calculations(); track property.name) { @@ -81,6 +92,7 @@ + } @for (property of measureControls(); track property.name) { diff --git a/packages/angular/entity/property-select/property-select.component.scss b/packages/angular/entity/property-select/property-select.component.scss index ea434c639..fec2609ab 100644 --- a/packages/angular/entity/property-select/property-select.component.scss +++ b/packages/angular/entity/property-select/property-select.component.scss @@ -54,4 +54,11 @@ $prefix-color: (#{$prefix}, color); align-items: center; } } + + .ngm-property-select__panel { + .mat-mdc-option.ngm-option__dimension, + .mat-mdc-option.ngm-option__hierarchy { + padding-left: 10px; + } + } } \ No newline at end of file diff --git a/packages/angular/entity/property-select/property-select.component.ts b/packages/angular/entity/property-select/property-select.component.ts index 4a6d4ce52..0af6179cb 100644 --- a/packages/angular/entity/property-select/property-select.component.ts +++ b/packages/angular/entity/property-select/property-select.component.ts @@ -37,6 +37,10 @@ import { isPropertyDimension, PropertyMeasure, isVariableProperty, + RuntimeLevelType, + isPropertyHierarchy, + isPropertyLevel, + isDimension, } from '@metad/ocap-core' import { cloneDeep, includes, isEmpty, isEqual, isNil, isString, negate, pick, uniq } from 'lodash-es' import { BehaviorSubject, combineLatest, firstValueFrom, Observable, of } from 'rxjs' @@ -66,7 +70,6 @@ import { NgmEntityPropertyComponent, propertyIcon } from '../property/property.c import { NgmFormattingComponent } from '../formatting/formatting.component' - @Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, @@ -106,6 +109,8 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi DISPLAY_BEHAVIOUR = DisplayBehaviour DisplayDensity = DisplayDensity CalculationType = CalculationType + isVisible = isVisible + LevelType = RuntimeLevelType @HostBinding('class.ngm-property-select') isPropertySelect = true @@ -140,7 +145,6 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi |-------------------------------------------------------------------------- */ readonly label = input() - // readonly value = signal(null) readonly capacities = input() readonly required = input(false, { @@ -178,6 +182,7 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi @ViewChild('propertySelect', { read: MatSelect }) private _propertySelect: MatSelect + readonly keyControl = new FormControl() readonly formGroup = new FormGroup({ name: new FormControl(null), caption: new FormControl(null), @@ -270,21 +275,36 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi readonly parameters$ = this.entityType$.pipe( map(entityType => Object.values(entityType?.parameters || {})) ) - + // Select options: dimension, hierarchy or levels readonly dimensions$: Observable> = combineLatest([ this.entityProperties$, this.restrictedDimensions$.pipe(distinctUntilChanged()), - this.searchControl.valueChanges.pipe(startWith('')) ]).pipe( - map(([properties, restrictedDimensions, text]) => filterProperty( + map(([properties, restrictedDimensions]) => { + const options = [] properties.filter(item => item.role === AggregationRole.dimension && (isEmpty(restrictedDimensions) ? true : includes(restrictedDimensions, item.name)) && isVisible(item) - ), - text - ) - ), + ).forEach((dimension: PropertyDimension) => { + options.push(dimension) + dimension.hierarchies?.forEach((hierarchy) => { + if (hierarchy.name !== dimension.name) { + options.push(hierarchy) + } + const levels = hierarchy.levels?.filter((level) => level.levelType !== RuntimeLevelType.ALL) + if (levels?.length > 1) { + levels.forEach((level) => { + options.push(level) + }) + } + }) + }) + + return options + }), + combineLatestWith(this.searchControl.valueChanges.pipe(startWith(''))), + map(([options, search]) => filterProperty(options, search)), shareReplay(1) ) @@ -629,25 +649,41 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi /** * When dimension changed */ - private dimensionSub = this.dimensionControl.valueChanges.pipe(distinctUntilChanged(), pairwise(), takeUntilDestroyed()) + private dimensionSub = this.keyControl.valueChanges.pipe(distinctUntilChanged(), pairwise(), takeUntilDestroyed()) .subscribe(([,dimension]) => { - const property = getEntityProperty(this.entityType(), dimension) + const property = getEntityProperty2(this.entityType(), dimension) if (isPropertyMeasure(property)) { this.formGroup.setValue({ ...this._formValue, dimension, } as any) - } else if(isPropertyDimension(property)) { - const hierarchyName = property.defaultHierarchy || dimension - let hierarchyProperty = getEntityHierarchy(this.entityType(), { dimension, hierarchy: hierarchyName}) - if (!hierarchyProperty) { - hierarchyProperty = property.hierarchies[0] + } else { + let hierarchy = null + let level = null + if(isPropertyDimension(property)) { + const hierarchyName = property.defaultHierarchy || dimension + let hierarchyProperty = getEntityHierarchy(this.entityType(), { dimension, hierarchy: hierarchyName}) + if (!hierarchyProperty) { + hierarchyProperty = property.hierarchies[0] + } + hierarchy = hierarchyProperty?.name + } + if (isPropertyHierarchy(property)) { + hierarchy = property.name + dimension = property.dimension + } + if (isPropertyLevel(property)) { + level = property.name + hierarchy = property.hierarchy + dimension = property.dimension } + // Reset all fields and set default hierarchy this.formGroup.setValue({ ...this._formValue, dimension, - hierarchy: hierarchyProperty?.name + hierarchy, + level } as any) } }) @@ -673,6 +709,11 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi // 避免双向绑定的循环更新 if (obj && !isEqual(this.#value, this.formGroup.value)) { this.patchValue(this.#value) + if (isMeasure(obj)) { + this.keyControl.setValue(obj.measure) + } else if (isDimension(obj)) { + this.keyControl.setValue(obj.level || obj.hierarchy || obj.dimension) + } } } registerOnChange(fn: any): void { @@ -697,6 +738,7 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi debounceTime(100), // Update value when property is initialized filter(() => !!this.property$.value), + distinctUntilChanged(isEqual), takeUntilDestroyed(this._destroyRef), ).subscribe((value) => { if (this.property$.value?.role === AggregationRole.measure) { @@ -748,6 +790,11 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi return item.name } + filterLevels(hierarchy: PropertyHierarchy) { + const levels = hierarchy.levels?.filter(isVisible).filter((level) => level.levelType !== RuntimeLevelType.ALL) + return levels?.length > 1 ? levels : [] + } + patchCalculationProperty(property: CalculationProperty) { this.patchValue({ dimension: C_MEASURES, diff --git a/packages/core/src/lib/models/helper.ts b/packages/core/src/lib/models/helper.ts index c79c7d6b1..c27a4e993 100644 --- a/packages/core/src/lib/models/helper.ts +++ b/packages/core/src/lib/models/helper.ts @@ -334,8 +334,6 @@ export function hasLevel(dimension: Dimension | string) { return isDimension(dimension) ? !!dimension.level : false } -export const isPropertyLevel = (toBe): toBe is PropertyLevel => toBe.aggregationRole === AggregationRole.level - /** * The property is Calendar Semantic * @@ -753,6 +751,10 @@ export const isDimensionUsage = (toBe): toBe is DimensionUsage => !isNil((toBe a export const isPropertyDimension = (toBe: unknown): toBe is PropertyDimension => (toBe as EntityProperty)?.role === AggregationRole.dimension +export const isPropertyHierarchy = (toBe): toBe is PropertyHierarchy => + (toBe as EntityProperty)?.role === AggregationRole.hierarchy +export const isPropertyLevel = (toBe): toBe is PropertyLevel => + (toBe as EntityProperty)?.role === AggregationRole.level export const isPropertyMeasure = (toBe): toBe is PropertyMeasure => (toBe as EntityProperty)?.role === AggregationRole.measure export const isEntityType = (toBe): toBe is EntityType => !(toBe instanceof Error) && !isNil((toBe as EntityType)?.name) diff --git a/packages/core/src/lib/models/sdl.ts b/packages/core/src/lib/models/sdl.ts index c87d48c18..20cb548fa 100644 --- a/packages/core/src/lib/models/sdl.ts +++ b/packages/core/src/lib/models/sdl.ts @@ -256,6 +256,32 @@ export interface PropertyHierarchy extends EntityProperty { levels?: Array } +export enum TimeLevelType { + TimeYears = 'TimeYears', + TimeQuarters = 'TimeQuarters', + TimeMonths = 'TimeMonths', + TimeWeeks = 'TimeWeeks', + TimeDays = 'TimeDays' +} + +/** + * Runtime level type + * Type of the level: + * https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2012/ms126038(v=sql.110) + * https://github.com/OpenlinkFinancial/MXMLABridge/blob/master/src/custom/mondrian/xmla/handler/RowsetDefinition.java + */ +export enum RuntimeLevelType { + REGULAR = 0, + ALL = 1, + CALCULATED = 2, + // GEO_CONTINENT = 1, + TIME_YEAR = 20, + TIME_QUARTER = 68, + TIME_MONTH = 132, + TIME_WEEK = 260, + TIME_DAY = 516 +} + export interface PropertyLevel extends EntityProperty { hierarchy?: PropertyName column?: string @@ -274,7 +300,7 @@ export interface PropertyLevel extends EntityProperty { /** * The type of level, such as 'TimeYears', 'TimeMonths', 'TimeDays' if dimension is a time dimension */ - levelType?: TimeLevelType | number | string + levelType?: TimeLevelType | RuntimeLevelType | string properties?: Array // hierarchyLevelFor?: PropertyName parentChild?: boolean @@ -286,14 +312,6 @@ export interface PropertyLevel extends EntityProperty { parentExpression?: SQLExpression } -export enum TimeLevelType { - TimeYears = 'TimeYears', - TimeQuarters = 'TimeQuarters', - TimeMonths = 'TimeMonths', - TimeWeeks = 'TimeWeeks', - TimeDays = 'TimeDays' -} - export interface LevelProperty extends PropertyAttributes { column?: string propertyExpression?: SQLExpression diff --git a/packages/xmla/src/lib/ds-xmla.service.ts b/packages/xmla/src/lib/ds-xmla.service.ts index 7b929b020..f775cbe3b 100644 --- a/packages/xmla/src/lib/ds-xmla.service.ts +++ b/packages/xmla/src/lib/ds-xmla.service.ts @@ -37,6 +37,7 @@ import { PropertyLevel, PropertyMeasure, QueryReturn, + RuntimeLevelType, Schema, Semantics, serializeArgs, @@ -65,7 +66,6 @@ import { Hierarchy, isWrapBrackets, Level, - LEVEL_TYPE, MDOptions, MDXDialect, MDXDimension, @@ -667,27 +667,27 @@ export class XmlaDataSource extends AbstractDataSource { levelProperty.hierarchy = levelProperty.hierarchyUniqueName switch (level.LEVEL_TYPE) { - case LEVEL_TYPE.MDLEVEL_TYPE_TIME_YEAR: + case RuntimeLevelType.TIME_YEAR: levelProperty.semantics = { semantic: Semantics['Calendar.Year'] } break - case LEVEL_TYPE.MDLEVEL_TYPE_TIME_QUARTER: + case RuntimeLevelType.TIME_QUARTER: levelProperty.semantics = { semantic: Semantics['Calendar.Quarter'] } break - case LEVEL_TYPE.MDLEVEL_TYPE_TIME_MONTH: + case RuntimeLevelType.TIME_MONTH: levelProperty.semantics = { semantic: Semantics['Calendar.Month'] } break - case LEVEL_TYPE.MDLEVEL_TYPE_TIME_WEEK: + case RuntimeLevelType.TIME_WEEK: levelProperty.semantics = { semantic: Semantics['Calendar.Week'] } break - case LEVEL_TYPE.MDLEVEL_TYPE_TIME_DAY: + case RuntimeLevelType.TIME_DAY: levelProperty.semantics = { semantic: Semantics['Calendar.Day'] } diff --git a/packages/xmla/src/lib/mdx-query.ts b/packages/xmla/src/lib/mdx-query.ts index 7b6f913fc..24e978b7d 100644 --- a/packages/xmla/src/lib/mdx-query.ts +++ b/packages/xmla/src/lib/mdx-query.ts @@ -25,6 +25,7 @@ import { parameterFormatter, Property, QueryOptions, + RuntimeLevelType, Semantics } from '@metad/ocap-core' import { findIndex, flatten, groupBy, isEmpty, merge, negate, omit, padStart, uniq } from 'lodash' @@ -34,7 +35,6 @@ import { Ascendants, Descendants, DescendantsFlag, Distinct, Except, Members, Me import { IntrinsicMemberProperties } from './reference/index' import { C_MDX_FIELD_NAME_REGEX, - LEVEL_TYPE, MDXDialect, MDXDimension, MDXHierarchy, @@ -42,7 +42,6 @@ import { MDXQuery, MDXRank, wrapBrackets, - wrapHierarchyValue } from './types/index' export function filterNotUnitText(dimensions: Array, entityType: EntityType) { @@ -622,7 +621,7 @@ export function allocateAxesFilter( export function serializeHierarchyDefaultLevel(entityType: EntityType, {dimension, hierarchy}: Dimension) { const property = getEntityHierarchy(entityType, {dimension, hierarchy}) - const levels = property.levels.filter((level) => level.levelType !== LEVEL_TYPE.MDLEVEL_TYPE_ALL) + const levels = property.levels.filter((level) => level.levelType !== RuntimeLevelType.ALL) const level = levels[0] if (!level) { throw new Error(`Can't find any levels in hierarchy ${hierarchy} of cube ${entityType.name} except all level`) diff --git a/packages/xmla/src/lib/types/metadata.ts b/packages/xmla/src/lib/types/metadata.ts index ef98d615d..65a5bb532 100644 --- a/packages/xmla/src/lib/types/metadata.ts +++ b/packages/xmla/src/lib/types/metadata.ts @@ -1,6 +1,6 @@ import { IDimensionMember, Property, PropertyHierarchy, PropertyLevel } from '@metad/ocap-core' import { lowerCase, lowerFirst, upperFirst } from 'lodash' -import { DIMENSION_TYPE, LEVEL_TYPE, XmlaMember } from './rowset' +import { DIMENSION_TYPE, XmlaMember } from './rowset' /** * MDX Cube Type @@ -231,7 +231,6 @@ export interface MDXLevel extends PropertyLevel, MDXCube { levelCaption: string levelNumber: number levelCardinality: number - levelType: LEVEL_TYPE customRollupSettings: number levelUniqueSettings: number levelIsVisible: boolean diff --git a/packages/xmla/src/lib/types/rowset.ts b/packages/xmla/src/lib/types/rowset.ts index ef81d88c0..0793a3937 100644 --- a/packages/xmla/src/lib/types/rowset.ts +++ b/packages/xmla/src/lib/types/rowset.ts @@ -1,4 +1,4 @@ -import { HttpHeaders } from '@metad/ocap-core' +import { HttpHeaders, RuntimeLevelType } from '@metad/ocap-core' export interface Rowset { fetchAllAsObject(): any @@ -135,7 +135,7 @@ export interface Level { LEVEL_IS_VISIBLE: boolean LEVEL_NAME: string LEVEL_NUMBER: number - LEVEL_TYPE: LEVEL_TYPE + LEVEL_TYPE: RuntimeLevelType LEVEL_UNIQUE_NAME: string SCHEMA_NAME: string } @@ -304,24 +304,6 @@ export enum DIMENSION_TYPE { MD_DIMTYPE_GEOGRAPHY = 17 } -/** - * Type of the level: - * https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2012/ms126038(v=sql.110) - * https://github.com/OpenlinkFinancial/MXMLABridge/blob/master/src/custom/mondrian/xmla/handler/RowsetDefinition.java - */ -export enum LEVEL_TYPE { - MDLEVEL_TYPE_REGULAR = 0, - MDLEVEL_TYPE_ALL = 1, - MDLEVEL_TYPE_CALCULATED = 2, - // MDLEVEL_TYPE_GEO_CONTINENT = 1, - MDLEVEL_TYPE_TIME_YEAR = 20, - MDLEVEL_TYPE_TIME_QUARTER = 68, - MDLEVEL_TYPE_TIME_MONTH = 132, - MDLEVEL_TYPE_TIME_WEEK = 260, - MDLEVEL_TYPE_TIME_DAY = 516 - //... -} - /** * MDMember type { From 3fcd3e0d0c9f841094783660079abc9538eead6d Mon Sep 17 00:00:00 2001 From: meta-d Date: Tue, 23 Jul 2024 22:08:17 +0800 Subject: [PATCH 17/53] feat: modeler agent optimization --- .../project/copilot/architect/graph.ts | 3 +- .../model/copilot/modeler/command.ts | 13 +- .../model/copilot/modeler/graph.ts | 130 +++++++++--------- .../model/copilot/modeler/planner.ts | 3 +- .../model/copilot/modeler/supervisor.ts | 119 ++++++++++++++-- .../model/copilot/modeler/types.ts | 22 +-- .../semantic-model/model/copilot/types.ts | 8 +- .../app/features/story/copilot/story/graph.ts | 3 +- packages/copilot/src/lib/graph/team.ts | 63 ++++++--- 9 files changed, 238 insertions(+), 126 deletions(-) diff --git a/apps/cloud/src/app/features/project/copilot/architect/graph.ts b/apps/cloud/src/app/features/project/copilot/architect/graph.ts index 2683e0a0b..ac3fc45c9 100644 --- a/apps/cloud/src/app/features/project/copilot/architect/graph.ts +++ b/apps/cloud/src/app/features/project/copilot/architect/graph.ts @@ -100,7 +100,8 @@ Methods for indicator design: 2. ${promptIndicatorCode(`{indicatorCodes}`)} 3. Please plan the indicator system first, and then decide to call route to create it one by one. -` +`, + `If you need to execute a task, you need to get confirmation before calling the route function.` ) return RunnableLambda.from(async (state: IndicatorArchitectState) => { diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts index eca43dd9e..82cd1c82e 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts @@ -1,14 +1,14 @@ import { inject } from '@angular/core' -import { CopilotAgentType, CreateGraphOptions } from '@metad/copilot' +import { CopilotAgentType, CreateGraphOptions, Team } from '@metad/copilot' import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' +import { injectAgentFewShotTemplate } from 'apps/cloud/src/app/@core/copilot' import { NGXLogger } from 'ngx-logger' import { SemanticModelService } from '../../model.service' +import { CUBE_MODELER_NAME } from '../cube' +import { DIMENSION_MODELER_NAME } from '../dimension' import { injectCreateModelerGraph } from './graph' import { injectCreateModelerPlanner } from './planner' -import { PLANNER_NAME } from './types' -import { DIMENSION_MODELER_NAME } from '../dimension' -import { CUBE_MODELER_NAME } from '../cube' export function injectModelerCommand() { const logger = inject(NGXLogger) @@ -33,6 +33,7 @@ export function injectModelerCommand() { }) const commandName = 'modeler' + const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) return injectCopilotCommand(commandName, { alias: 'm', description: translate.instant('PAC.MODEL.Copilot.CommandModelerDesc', { @@ -41,8 +42,7 @@ export function injectModelerCommand() { agent: { type: CopilotAgentType.Graph, conversation: true, - interruptBefore: [DIMENSION_MODELER_NAME, CUBE_MODELER_NAME], - interruptAfter: [PLANNER_NAME] + interruptBefore: [Team.TOOLS_NAME, DIMENSION_MODELER_NAME, CUBE_MODELER_NAME] }, historyCursor: () => { return modelService.getHistoryCursor() @@ -50,6 +50,7 @@ export function injectModelerCommand() { revert: async (index: number) => { modelService.gotoHistoryCursor(index) }, + fewShotPrompt, createGraph: async ({ llm }: CreateGraphOptions) => { return await createModelerGraph({ llm diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts index e2eb41f12..63d398b03 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts @@ -1,101 +1,107 @@ +import { inject } from '@angular/core' import { RunnableLambda } from '@langchain/core/runnables' +import { ToolNode } from '@langchain/langgraph/prebuilt' import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web' import { CreateGraphOptions, Team } from '@metad/copilot' +import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME, injectRunCubeModeler } from '../cube' import { DIMENSION_MODELER_NAME, injectRunDimensionModeler } from '../dimension/' +import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' import { injectRunModelerPlanner } from './planner' -import { createSupervisor } from './supervisor' -import { PLANNER_NAME, SUPERVISOR_NAME, State } from './types' -import { HumanMessage } from '@langchain/core/messages' +import { createSupervisorAgent } from './supervisor' +import { ModelerState } from './types' -const superState: StateGraphArgs['channels'] = { +const superState: StateGraphArgs['channels'] = { ...Team.createState(), - plan: { - value: (x?: string[], y?: string[]) => y ?? x ?? [], - default: () => [] - } } export function injectCreateModelerGraph() { + const modelService = inject(SemanticModelService) const createModelerPlanner = injectRunModelerPlanner() const createDimensionModeler = injectRunDimensionModeler() const createCubeModeler = injectRunCubeModeler() + const selectTablesTool = injectSelectTablesTool() + const queryTablesTool = injectQueryTablesTool() + + const dimensions = modelService.dimensions return async ({ llm }: CreateGraphOptions) => { - const supervisorNode = await createSupervisor(llm, [ - { - name: PLANNER_NAME, - description: 'Create a plan for modeling' - }, - { - name: DIMENSION_MODELER_NAME, - description: 'Create a dimension, only one at a time' - }, - { - name: CUBE_MODELER_NAME, - description: 'Create a cube, only one at a time' - } - ]) + const tools = [selectTablesTool, queryTablesTool] + const supervisorAgent = await createSupervisorAgent({ llm, dimensions, tools }) + const dimensionAgent = await createDimensionModeler({ llm }) + const cubeAgent = await createCubeModeler({ llm }) + + // const supervisorNode = await createSupervisor(llm, [ + // { + // name: PLANNER_NAME, + // description: 'Create a plan for modeling' + // }, + // { + // name: DIMENSION_MODELER_NAME, + // description: 'Create a dimension, only one at a time' + // }, + // { + // name: CUBE_MODELER_NAME, + // description: 'Create a cube, only one at a time' + // } + // ]) const plannerAgent = await createModelerPlanner({ llm }) const superGraph = new StateGraph({ channels: superState }) // Add steps nodes - .addNode(SUPERVISOR_NAME, RunnableLambda.from(async (state: State) => { - const _state = await supervisorNode.invoke(state) - return { - ..._state, - messages: [ - new HumanMessage(`Call ${_state.next} with instructions: ${_state.instructions}`) - ] - } - })) - .addNode(PLANNER_NAME, - RunnableLambda.from(async (state: State) => { - return plannerAgent.invoke({ - input: state.instructions, - role: state.role, - context: state.context, - language: state.language, - messages: [] - }) - }) - ) + .addNode(Team.SUPERVISOR_NAME, supervisorAgent.withConfig({ runName: Team.SUPERVISOR_NAME })) + .addNode(Team.TOOLS_NAME, new ToolNode(tools)) + // .addNode(SUPERVISOR_NAME, RunnableLambda.from(async (state: State) => { + // const _state = await supervisorNode.invoke(state) + // return { + // ..._state, + // messages: [ + // new HumanMessage(`Call ${_state.next} with instructions: ${_state.instructions}`) + // ] + // } + // })) + // .addNode(PLANNER_NAME, + // RunnableLambda.from(async (state: State) => { + // return plannerAgent.invoke({ + // input: state.instructions, + // role: state.role, + // context: state.context, + // language: state.language, + // messages: [] + // }) + // }) + // ) .addNode( DIMENSION_MODELER_NAME, - RunnableLambda.from(async (state: State) => { - return { + RunnableLambda.from(async (state: ModelerState) => { + const { messages } = await dimensionAgent.invoke({ input: state.instructions, role: state.role, context: state.context, language: state.language, messages: [] - } - }).pipe(await createDimensionModeler({ llm })) + }) + return Team.responseToolMessage(state.tool_call_id, messages) + }) ) .addNode( CUBE_MODELER_NAME, - RunnableLambda.from(async (state: State) => { - return { + RunnableLambda.from(async (state: ModelerState) => { + const { messages } = await cubeAgent.invoke({ input: state.instructions, role: state.role, context: state.context, language: state.language, messages: [] - } - }).pipe(await createCubeModeler({ llm })) + }) + return Team.responseToolMessage(state.tool_call_id, messages) + }) ) - - superGraph.addEdge(PLANNER_NAME, SUPERVISOR_NAME) - superGraph.addEdge(DIMENSION_MODELER_NAME, SUPERVISOR_NAME) - superGraph.addEdge(CUBE_MODELER_NAME, SUPERVISOR_NAME) - superGraph.addConditionalEdges(SUPERVISOR_NAME, (x) => x.next, { - [PLANNER_NAME]: PLANNER_NAME, - [DIMENSION_MODELER_NAME]: DIMENSION_MODELER_NAME, - [CUBE_MODELER_NAME]: CUBE_MODELER_NAME, - FINISH: END - }) - - superGraph.addEdge(START, SUPERVISOR_NAME) + .addEdge(Team.TOOLS_NAME, Team.SUPERVISOR_NAME) + .addEdge(DIMENSION_MODELER_NAME, Team.SUPERVISOR_NAME) + .addEdge(CUBE_MODELER_NAME, Team.SUPERVISOR_NAME) + .addConditionalEdges(Team.SUPERVISOR_NAME, Team.supervisorRouter) + .addEdge(START, Team.SUPERVISOR_NAME) return superGraph } diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts index f75c853c4..d794c8266 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts @@ -18,7 +18,8 @@ const SYSTEM_PROMPT = {language} 1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes. Or use the 'selectTables' tool to get all tables then select the required physical tables from them. -2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. +2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. + If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions. Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension. diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts index c0eecd350..565ee73a7 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts @@ -1,21 +1,114 @@ +import { Signal } from '@angular/core' +import { RunnableLambda } from '@langchain/core/runnables' +import { DynamicStructuredTool } from '@langchain/core/tools' import { ChatOpenAI } from '@langchain/openai' import { Team } from '@metad/copilot' +import { PropertyDimension } from '@metad/ocap-core' +import { CUBE_MODELER_NAME } from '../cube' +import { DIMENSION_MODELER_NAME } from '../dimension' +import { getTablesFromDimension } from '../types' +import { ModelerState } from './types' -export async function createSupervisor(llm: ChatOpenAI, members: {name: string; description: string;}[]) { - const supervisorNode = await Team.createSupervisor( +export async function createSupervisorAgent({ + llm, + dimensions, + tools +}: { + llm: ChatOpenAI + dimensions: Signal + tools: DynamicStructuredTool[] +}) { + const getDimensions = async () => { + return dimensions().length + ? `Existing shared dimensions:\n` + + dimensions() + .map( + (d) => + `- name: ${d.name} + caption: ${d.caption || ''} + tables: ${getTablesFromDimension(d).join(', ')}` + ) + .join('\n') + : `There are no existing shared dimensions.` + } + + const agent = await Team.createSupervisorAgent( llm, - members, - `You are a supervisor tasked with managing a conversation between the following teams: -{team_members} + [ + { + name: DIMENSION_MODELER_NAME, + description: 'Create a dimension, only one at a time' + }, + { + name: CUBE_MODELER_NAME, + description: 'Create a cube, only one at a time' + } + ], + tools, + `You are a cube modeler for data analysis, now you need create a plan for the final goal. +{role} +{language} +{context} + +1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes. + Or use the 'selectTables' tool to get all tables then select the required physical tables from them. And use 'queryTables' tool get columns metadata for the required tables. +2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. + If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. + If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. + If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions. + Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension. + For example, create a dimension called Department with the fields: First-level Department, Second-level Department, Third-level Department, and add First-level Department, Second-level Department, and Third-level Department as the levels of the dimension in order. + If you are creating a shared dimension, please provide your reason. + +For example: +Objective is: Create a cube using the table. +Table context: + Table is: +- name: sales_data + caption: 销售数据 + columns: + - name: company_code + caption: 公司代码 + type: character varying + - name: product_code + caption: 产品代码 + type: character varying + - name: sales_amount + caption: 销售金额 + type: numeric + +- name: product_data + caption: 产品数据 + columns: + - name: product_code + caption: 产品代码 + type: character varying + - name: product_name + caption: 产品名称 + type: character varying + - name: product_category + caption: 产品类别 + type: character varying +Answer is: Think about the shared dimension and inline dimension of the model: +The company_code field in the sales_data fact table, it do not have a dimension table, so it is a inline dimension. +The product_code field has a dimension table 'product_data', so it is a shared dimension. +The plan are as follows: +1. Create a shared dimension 'Product' for table product_data. +2. Create a cube with share dimension 'Product' and inline dimensions: 'Company' for table sales_data. -Given the following user request ` + - `Create a plan then execute the plan steps one by one,` + - ' by respond with the worker to act next. Each worker will perform a' + - ' step task and respond with their results and status. When finished,' + - ' respond with FINISH.\n\n' + - // 'Current plan is {plan}.\n\n' + - ' Select strategically to minimize the number of steps taken.', +Avoid creating already existing shared dimensions: +{dimensions} +just use them directly in the cube creation task. +Please plan the cube model first, and then decide to call route to create it step by step. +`, + `Use only one tool at a time` ) - return supervisorNode + return RunnableLambda.from(async (state: ModelerState) => { + const dimensions = await getDimensions() + return { + ...state, + dimensions + } + }).pipe(agent) } diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts index fbf059ea9..3504eea5b 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts @@ -1,25 +1,7 @@ -import { RunnableLambda } from '@langchain/core/runnables' import { Team } from '@metad/copilot' -import { AgentState } from '@metad/copilot-angular' export const PLANNER_NAME = 'Planner' export const SUPERVISOR_NAME = 'Supervisor' -// Define the top-level State interface -export interface State extends Team.State { - plan: string[] -} - -export interface IPlanState extends AgentState { - objective: string -} - -export const getMessages = RunnableLambda.from((state: AgentState) => { - return { messages: state.messages } -}) - -export const joinGraph = RunnableLambda.from((response: any) => { - return { - messages: [response.messages[response.messages.length - 1]] - } -}) +export interface ModelerState extends Team.State { +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/types.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/types.ts index 926e058ed..a64f8f0ce 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/types.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/types.ts @@ -1,9 +1,11 @@ import { CopilotChatConversation, CopilotService } from '@metad/copilot' -import { EntityType } from '@metad/ocap-core' +import { EntityType, PropertyDimension } from '@metad/ocap-core' import { NGXLogger } from 'ngx-logger' import { ModelEntityService } from '../entity/entity.service' import { SemanticModelService } from '../model.service' +import { flatten } from 'lodash-es' +export const ModelCopilotCommandArea = 'Model' export interface ModelCopilotChatConversation extends CopilotChatConversation { dataSource: string @@ -17,4 +19,6 @@ export interface ModelCopilotChatConversation extends CopilotChatConversation { sharedDimensionsPrompt: string } -export const ModelCopilotCommandArea = 'Model' +export function getTablesFromDimension(dimension: PropertyDimension) { + return flatten(dimension.hierarchies.map((hierarchy) => hierarchy.tables.map((table) => table.name))) +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/copilot/story/graph.ts b/apps/cloud/src/app/features/story/copilot/story/graph.ts index 172fe0598..4b874dc2c 100644 --- a/apps/cloud/src/app/features/story/copilot/story/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/story/graph.ts @@ -65,7 +65,8 @@ export function injectCreateStoryGraph() { {context} Story dashbaord 通常由多个页面组成,每个页面是一个分析主题,每个主题的页面通常由一个过滤器栏、多个主要的维度输入控制器、多个指标、多个图形、一个或多个表格组成。 -` +`, + `If you need to execute a task, you need to get confirmation before calling the route function.` ) const widgetAgent = await createWidgetGraph({ llm }) diff --git a/packages/copilot/src/lib/graph/team.ts b/packages/copilot/src/lib/graph/team.ts index 45e869c27..d9305115e 100644 --- a/packages/copilot/src/lib/graph/team.ts +++ b/packages/copilot/src/lib/graph/team.ts @@ -1,13 +1,14 @@ -import { BaseMessage, HumanMessage, isAIMessage } from '@langchain/core/messages' +import { BaseMessage, HumanMessage, isAIMessage, ToolMessage } from '@langchain/core/messages' import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts' import { Runnable, RunnableLambda } from '@langchain/core/runnables' +import { DynamicStructuredTool } from '@langchain/core/tools' +import { END } from '@langchain/langgraph/web' import { ChatOpenAI } from '@langchain/openai' import { JsonOutputToolsParser } from 'langchain/output_parsers' import { AgentState, createCopilotAgentState } from './types' -import { END } from '@langchain/langgraph/web' -import { DynamicStructuredTool } from '@langchain/core/tools' export const SUPERVISOR_NAME = 'Supervisor' +export const TOOLS_NAME = 'tools' export interface State extends AgentState { next: string @@ -38,7 +39,6 @@ export function createState() { } } - export const getInstructions = RunnableLambda.from((state: State) => { return { input: state.instructions, @@ -61,12 +61,13 @@ export const joinGraph = RunnableLambda.from((response: AgentState) => { } }) -export const SupervisorSystemPrompt = 'You are a supervisor tasked with managing a conversation between the' + - ' following workers: {team_members}. Given the following user request,' + - ' respond with the worker to act next. Each worker will perform a' + - ' task and respond with their results and status. When finished,' + - ' respond with FINISH.\n\n' + - ' Select strategically to minimize the number of steps taken.' +export const SupervisorSystemPrompt = + 'You are a supervisor tasked with managing a conversation between the' + + ' following workers: {team_members}. Given the following user request,' + + ' respond with the worker to act next. Each worker will perform a' + + ' task and respond with their results and status. When finished,' + + ' respond with FINISH.\n\n' + + ' Select strategically to minimize the number of steps taken.' export const RouteFunctionName = 'route' export function createRouteFunctionDef(members: string[]) { @@ -96,8 +97,12 @@ export function createRouteFunctionDef(members: string[]) { } } -export async function createSupervisor(llm: ChatOpenAI, members: {name: string; description: string;}[], systemPrompt?: string): Promise { - const options = ['FINISH', ...members.map(({name}) => name)] +export async function createSupervisor( + llm: ChatOpenAI, + members: { name: string; description: string }[], + systemPrompt?: string +): Promise { + const options = ['FINISH', ...members.map(({ name }) => name)] const functionDef = createRouteFunctionDef(options) const toolDef = { type: 'function' as const, @@ -110,7 +115,7 @@ export async function createSupervisor(llm: ChatOpenAI, members: {name: string; ]) prompt = await prompt.partial({ options: options.join(', '), - team_members: members.map(({name, description}) => `**${name}**: ${description}`).join('\n') + team_members: members.map(({ name, description }) => `**${name}**: ${description}`).join('\n') }) const supervisor = prompt @@ -127,9 +132,14 @@ export async function createSupervisor(llm: ChatOpenAI, members: {name: string; return supervisor } - - export async function createSupervisorAgent(llm: ChatOpenAI, members: {name: string; description: string;}[], tools: DynamicStructuredTool[], system: string) { - const functionDef = createRouteFunctionDef(members.map((({name}) => name))) +export async function createSupervisorAgent( + llm: ChatOpenAI, + members: { name: string; description: string }[], + tools: DynamicStructuredTool[], + system: string, + suffix = '' +) { + const functionDef = createRouteFunctionDef(members.map(({ name }) => name)) const toolDef = { type: 'function' as const, function: functionDef @@ -139,12 +149,15 @@ export async function createSupervisor(llm: ChatOpenAI, members: {name: string; let prompt = ChatPromptTemplate.fromMessages([ ['system', system], ['placeholder', '{messages}'], - ['system', `Given the conversation above, please give priority to answering questions with language only. If you need to execute a task, you need to get confirmation before calling the route function. + [ + 'system', + `Given the conversation above, please give priority to answering questions with language only. ${suffix} To perform a task, you can select one of the following: -{members}`] // +{members}` + ] ]) prompt = await prompt.partial({ - members: members.map(({name, description}) => `- ${name}: ${description}`).join('\n') + members: members.map(({ name, description }) => `- ${name}: ${description}`).join('\n') }) const modelRunnable = prompt.pipe(modelWithTools) @@ -187,4 +200,14 @@ export const supervisorRouter = (state: AgentState) => { } } -export const TOOLS_NAME = 'tools' \ No newline at end of file +export function responseToolMessage(id: string, messages: BaseMessage[]) { + return { + tool_call_id: null, + messages: [ + new ToolMessage({ + tool_call_id: id, + content: messages[messages.length - 1].content + }) + ] + } +} From 6fc0f679cc29df36634a5fe26b6d2a504d02fd43 Mon Sep 17 00:00:00 2001 From: meta-d Date: Tue, 23 Jul 2024 23:53:56 +0800 Subject: [PATCH 18/53] feat: property select style --- .../property-select.component.html | 78 +++++++++++-------- .../property-select.component.scss | 5 +- .../property-select.component.ts | 28 ++----- 3 files changed, 56 insertions(+), 55 deletions(-) diff --git a/packages/angular/entity/property-select/property-select.component.html b/packages/angular/entity/property-select/property-select.component.html index a6ae753f7..7234da53b 100644 --- a/packages/angular/entity/property-select/property-select.component.html +++ b/packages/angular/entity/property-select/property-select.component.html @@ -58,7 +58,7 @@ (keydown)="onSearchKeydown($event)" (click)="$event.stopPropagation()"/> - @if (showDimension) { + @if (showDimension()) { } - @if (showMeasure) { + @if (showMeasure()) { @for (property of calculations(); track property.name) { - + } @@ -93,8 +93,8 @@ } - - + @if (showMeasureControl()) { + @for (property of measureControls(); track property.name) { @@ -108,24 +108,30 @@ - - - + } + + @if (showMeasure()) { + + - - + } + @if (showParameter()) { + - - - + } + + @if (showMeasure()) { + + + }
@@ -168,27 +174,31 @@ > - - + @if (isMeasure$ | async) { + @if (showMeasureAttributes()) { + + } - - + @if (calculationProperty(); as calculationProperty) { + + } + } - + @if (isParameter()) { - + } - - + @if (showOrder()) { + + } diff --git a/packages/angular/entity/property-select/property-select.component.scss b/packages/angular/entity/property-select/property-select.component.scss index fec2609ab..fc702a163 100644 --- a/packages/angular/entity/property-select/property-select.component.scss +++ b/packages/angular/entity/property-select/property-select.component.scss @@ -57,8 +57,9 @@ $prefix-color: (#{$prefix}, color); .ngm-property-select__panel { .mat-mdc-option.ngm-option__dimension, - .mat-mdc-option.ngm-option__hierarchy { - padding-left: 10px; + .mat-mdc-option.ngm-option__hierarchy, + .mat-mdc-option.ngm-option__measure { + padding-left: 16px; } } } \ No newline at end of file diff --git a/packages/angular/entity/property-select/property-select.component.ts b/packages/angular/entity/property-select/property-select.component.ts index 0af6179cb..04f107508 100644 --- a/packages/angular/entity/property-select/property-select.component.ts +++ b/packages/angular/entity/property-select/property-select.component.ts @@ -597,24 +597,12 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi }) } - get showDimension() { - return this.capacities()?.includes(PropertyCapacity.Dimension) - } - get showMeasure() { - return this.capacities()?.includes(PropertyCapacity.Measure) - } - get showParameter() { - return this.capacities()?.includes(PropertyCapacity.Parameter) - } - get showMeasureControl() { - return this.capacities()?.includes(PropertyCapacity.MeasureControl) - } - get showMeasureAttributes() { - return this.capacities()?.includes(PropertyCapacity.MeasureAttributes) - } - get showOrder() { - return this.capacities()?.includes(PropertyCapacity.Order) - } + readonly showDimension = computed(() => this.capacities()?.includes(PropertyCapacity.Dimension)) + readonly showMeasure = computed(() => this.capacities()?.includes(PropertyCapacity.Measure)) + readonly showParameter = computed(() => this.capacities()?.includes(PropertyCapacity.Parameter)) + readonly showMeasureControl = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureControl)) + readonly showMeasureAttributes = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureAttributes)) + readonly showOrder = computed(() => this.capacities()?.includes(PropertyCapacity.Order)) readonly showMore = signal(false) @@ -649,8 +637,8 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi /** * When dimension changed */ - private dimensionSub = this.keyControl.valueChanges.pipe(distinctUntilChanged(), pairwise(), takeUntilDestroyed()) - .subscribe(([,dimension]) => { + private keySub = this.keyControl.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe((dimension) => { const property = getEntityProperty2(this.entityType(), dimension) if (isPropertyMeasure(property)) { this.formGroup.setValue({ From c2a0415f75101797eb27c6df9cbb0f4e08f7b823 Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 24 Jul 2024 16:58:04 +0800 Subject: [PATCH 19/53] feat: table command agent --- .../app/features/semantic-model/model/copilot/table/graph.ts | 4 +++- .../app/features/semantic-model/model/copilot/table/tools.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts index b7f4ed369..4a12d0db9 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts @@ -15,7 +15,9 @@ function createSystemPrompt(dialect: string) { {role} {language} The database dialect is '${dialect}'. -You need add short label to the created table and it's columns. +Consider the tables that need to be created based on the user's questions and call the 'createTable' tool to create them one by one. +You need to add short labels to the created table and its columns. +In PostgreSQL, the 'LABEL' syntax is incorrect, we can add labels by using Comment. {context}` ) } diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts index a96263d8c..85df168ff 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts @@ -12,7 +12,7 @@ export function injectCreateTableTool() { const createTableTool = new DynamicStructuredTool({ name: 'createTable', - description: 'Create or edit a table', + description: 'Create or edit a table, one table at a time', schema: z.object({ statement: z.string().describe('The statement of creating or modifing a table') }), From 4eaf5a1aff4ed60ac7f095bc52008fb4281a4c95 Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 24 Jul 2024 16:58:23 +0800 Subject: [PATCH 20/53] feat: tools call table --- .../chat/ai-message/ai-message.component.html | 158 ++++++++++++++++ .../chat/ai-message/ai-message.component.scss | 29 +++ .../chat/ai-message/ai-message.component.ts | 80 +++++++++ .../src/lib/chat/chat.component.html | 168 +----------------- .../src/lib/chat/chat.component.scss | 59 +----- .../src/lib/chat/chat.component.ts | 39 +--- .../lib/{ => chat}/token/token.component.ts | 22 +-- .../copilot-angular/src/lib/i18n/zhHans.ts | 4 + .../src/lib/services/engine.service.ts | 37 ++-- packages/copilot/src/lib/types/types.ts | 7 +- 10 files changed, 328 insertions(+), 275 deletions(-) create mode 100644 packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.html create mode 100644 packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss create mode 100644 packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.ts rename packages/copilot-angular/src/lib/{ => chat}/token/token.component.ts (55%) diff --git a/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.html b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.html new file mode 100644 index 000000000..b8ef1cad2 --- /dev/null +++ b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.html @@ -0,0 +1,158 @@ +@if (message().templateRef) { + +} @else { + + @if (messageData()) { + @switch (messageData().type) { + @case (MessageDataType.Route) { +
+ + + + + + + + + + + + + + + + + + +
+ {{'Copilot.Invoke' | translate: {Default: 'Invoke'} }} + {{messageData().data.next}}
+ {{'Copilot.Instructions' | translate: {Default: 'Instructions'} }} + + +
+ {{'Copilot.Reasoning' | translate: {Default: 'Reasoning'} }} + {{messageData().data.reasoning}}
+
+ } + @case (MessageDataType.ToolsCall) { +
+ + + + + + + + + + @for (row of messageData().data; track $index) { + + + + + } + +
{{'Copilot.Name' | translate: {Default: 'Name'} }}{{'Copilot.Args' | translate: {Default: 'Args'} }}
+ {{row.name}} + {{row.args}}
+
+ } + } + } @else if (message().content) { +
+ + + @if (showTokenizer() && message().content) { + + } + + @if (message().status === 'done') { +
+ @if (messageCopied().includes(message().id)) { + + } @else { + + } + + @if (conversation().command?.revert && !message().reverted) { + + } + + +
+ } +
+ } +} + +@if (message().error) { +
+ 🙈 + {{ message().error }} +
+} + + + + + + diff --git a/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss new file mode 100644 index 000000000..f0dab0e8e --- /dev/null +++ b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss @@ -0,0 +1,29 @@ +.ngm-copilot-ai-message__table { + @apply m-2 rounded-lg border border-slate-100 dark:border-slate-700; + + table { + @apply border-collapse; + } + + thead tr th, + tbody tr:not(:last-child) td { + @apply border-b border-slate-100 dark:border-slate-700; + } + + tr { + th, td { + @apply p-1 pl-2; + } + } + + .ngm-copilot__route-instructions-input { + @apply p-1 border border-transparent rounded-md outline outline-1 outline-offset-1 outline-transparent dark:border-transparent bg-transparent; + + &:hover { + @apply shadow-sm border-slate-200 dark:border-slate-800; + } + &:focus { + @apply shadow-sm border-slate-300 dark:border-slate-800 outline-blue-500; + } + } +} diff --git a/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.ts b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.ts new file mode 100644 index 000000000..8c26f441a --- /dev/null +++ b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.ts @@ -0,0 +1,80 @@ +import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard' +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatTooltipModule } from '@angular/material/tooltip' +import { CopilotChatConversation, CopilotChatMessage, CopilotCommand, MessageDataType } from '@metad/copilot' +import { TranslateModule } from '@ngx-translate/core' +import { MarkdownModule } from 'ngx-markdown' +import { NgmCopilotEngineService, NgmCopilotService } from '../../services' +import { CopilotChatTokenComponent } from '../token/token.component' +import { NgmCopilotChatMessage } from '../../types' + +@Component({ + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'ngm-copilot-ai-message', + templateUrl: 'ai-message.component.html', + styleUrls: ['ai-message.component.scss'], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule, + ClipboardModule, + + MarkdownModule, + MatTooltipModule, + + CopilotChatTokenComponent + ] +}) +export class CopilotAIMessageComponent { + MessageDataType = MessageDataType + + readonly copilotService = inject(NgmCopilotService) + readonly #clipboard: Clipboard = inject(Clipboard) + + readonly conversation = input() + readonly message = input() + readonly copilotEngine = input() + + readonly copied = output() + + readonly copilot = toSignal(this.copilotService.copilot$) + readonly copilotEnabled = toSignal(this.copilotService.enabled$) + readonly showTokenizer = computed(() => this.copilot()?.showTokenizer) + + readonly messageCopied = signal([]) + + readonly messageData = computed(() => { + const data = this.message().data + return data as {type: MessageDataType; data?: any;} + }) + + async revert(command: CopilotCommand, message: CopilotChatMessage) { + await command.revert?.(message.historyCursor) + message.reverted = true + } + + copyMessage(message: CopilotChatMessage) { + this.copied.emit(message.content) + this.#clipboard.copy(message.content) + this.messageCopied.update((ids) => [...ids, message.id]) + setTimeout(() => { + this.messageCopied.update((ids) => ids.filter((id) => id !== message.id)) + }, 3000) + } + + onCopy(copyButton) { + copyButton.copied = true + setTimeout(() => { + copyButton.copied = false + }, 3000) + } + + onRouteChange(conversationId: string, event: string) { + this.copilotEngine().updateConversationState(conversationId, {instructions: event}) + } +} diff --git a/packages/copilot-angular/src/lib/chat/chat.component.html b/packages/copilot-angular/src/lib/chat/chat.component.html index 6e45c13a1..65a7beed7 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.html +++ b/packages/copilot-angular/src/lib/chat/chat.component.html @@ -30,32 +30,6 @@
- - - @for (conversation of (copilotEnabled() ? conversations() : _mockConversations); track $index; let last = $last) { @if (conversation.command && !conversation.command.hidden) {
/{{conversation.command.name}}
@@ -76,88 +50,12 @@
🤖
}
-
- @if (message.templateRef) { - - } @else { - @if (message.content) { -
- - - @if (showTokenizer$() && message.content) { - - } - - @if (message.status === 'done') { -
- @if (messageCopied().includes(message.id)) { - - } @else { - - } - - @if (conversation.command?.revert && !message.reverted) { - - } - - -
- } -
- } - } - - @if (message.error) { -
- 🙈 - {{ message.error }} -
- } - -
+ +
} @@ -526,23 +424,6 @@ - - - - - @@ -570,40 +451,3 @@ } - - -
- - - - - - - - - - - - - - - - - -
- {{'Copilot.Invoke' | translate: {Default: 'Invoke'} }} - {{message.data.next}}
- {{'Copilot.Instructions' | translate: {Default: 'Instructions'} }} - - -
- {{'Copilot.Reasoning' | translate: {Default: 'Reasoning'} }} - {{message.data.reasoning}}
-
-
\ No newline at end of file diff --git a/packages/copilot-angular/src/lib/chat/chat.component.scss b/packages/copilot-angular/src/lib/chat/chat.component.scss index b748ad26d..cb2fda0c1 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.scss +++ b/packages/copilot-angular/src/lib/chat/chat.component.scss @@ -13,16 +13,6 @@ @apply flex flex-col relative; } -.ngm-copilot-chat__command { - // background-color: rgb(217 161 255); - // background: linear-gradient(90deg, rgb(223 188 247) 0%, rgb(250 177 177) 50%, rgb(255 220 171) 100%); - - // background: var(--red-to-orange-horizontal-gradient); - // background-clip: text; - // -webkit-background-clip: text; - // color: transparent; -} - .ngm-copilot-chat__command-tag { @apply bg-gray-200 text-neutral-800 dark:bg-gray-100/10 dark:text-neutral-300; } @@ -93,10 +83,6 @@ .assistant { .ngm-copilot-chat__message-content { @apply rounded-lg border border-dashed border-transparent; - - // &.thinking { - // @apply border-neutral-200 dark:border-neutral-800; - // } } } @@ -106,10 +92,6 @@ } } -// .ngm-copilot__user-message { -// @apply rounded-xl rounded-tr-sm bg-gray-200/50 dark:bg-neutral-700/30; -// } - .ngm-copilot-chat__answering { background-color: var(--ngm-copilot-bg-color, white); } @@ -120,44 +102,13 @@ width: 18px; } -.ngm-copilot-button { - @apply rounded-md text-xs w-9 h-9 flex items-center justify-center transition-colors duration-100 - text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-300 - bg-transparent hover:bg-neutral-100 dark:hover:bg-neutral-800; -} - -.ngm-copilot__route-table { - @apply m-2 rounded-lg border border-slate-100 dark:border-slate-700; - - table { - @apply border-collapse; - } - - tr:not(:last-child) { - td { - @apply border-b border-slate-100 dark:border-slate-700; - } - } - - tr { - td { - @apply p-1 pl-2; - } - } - - .ngm-copilot__route-instructions-input { - @apply p-1 border border-transparent rounded-md outline outline-1 outline-offset-1 outline-transparent dark:border-transparent bg-transparent; - - &:hover { - @apply shadow-sm border-slate-200 dark:border-slate-800; - } - &:focus { - @apply shadow-sm border-slate-300 dark:border-slate-800 outline-blue-500; - } +:host::ng-deep { + .ngm-copilot-button { + @apply rounded-md text-xs w-9 h-9 flex items-center justify-center transition-colors duration-100 + text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-300 + bg-transparent hover:bg-neutral-100 dark:hover:bg-neutral-800; } -} -:host::ng-deep { markdown { > * { @apply whitespace-pre-line; diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index 1c1c1fdae..e92c3ee15 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -1,4 +1,3 @@ -import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard' import { CdkDragDrop } from '@angular/cdk/drag-drop' import { ScrollingModule } from '@angular/cdk/scrolling' import { TextFieldModule } from '@angular/cdk/text-field' @@ -49,10 +48,9 @@ import { CopilotChatMessageRoleEnum, CopilotCommand, CopilotContextItem, + nanoid, } from '@metad/copilot' import { TranslateModule } from '@ngx-translate/core' -import { nanoid } from 'nanoid' -import { MarkdownModule } from 'ngx-markdown' import { NgxPopperjsContentComponent, NgxPopperjsModule, @@ -80,9 +78,10 @@ import { NgmHighlightDirective } from '../core/directives' import { NgmCopilotEnableComponent } from '../enable/enable.component' import { injectCommonCommands } from '../hooks/common' import { NgmCopilotEngineService, NgmCopilotService } from '../services/' -import { CopilotChatTokenComponent } from '../token/token.component' +import { CopilotChatTokenComponent } from './token/token.component' import { IUser, NgmCopilotChatMessage } from '../types' import { PlaceholderMessages } from './types' +import { CopilotAIMessageComponent } from './ai-message/ai-message.component' export const AUTO_SUGGESTION_DEBOUNCE_TIME = 1000 export const AUTO_SUGGESTION_STOP = ['\n', '.', ',', '@', '#'] @@ -99,7 +98,6 @@ export const AUTO_SUGGESTION_STOP = ['\n', '.', ',', '@', '#'] ReactiveFormsModule, RouterModule, TextFieldModule, - ClipboardModule, MatInputModule, MatIconModule, MatButtonModule, @@ -114,7 +112,6 @@ export const AUTO_SUGGESTION_STOP = ['\n', '.', ',', '@', '#'] MatProgressSpinnerModule, TranslateModule, NgxPopperjsModule, - MarkdownModule, ScrollingModule, NgmSearchComponent, @@ -123,7 +120,8 @@ export const AUTO_SUGGESTION_STOP = ['\n', '.', ',', '@', '#'] CopilotChatTokenComponent, NgmCopilotEnableComponent, UserAvatarComponent, - NgmScrollBackComponent + NgmScrollBackComponent, + CopilotAIMessageComponent ], host: { class: 'ngm-copilot-chat' @@ -141,8 +139,6 @@ export class NgmCopilotChatComponent { readonly copilotEngine$ = signal(this.#copilotEngine) - readonly #clipboard: Clipboard = inject(Clipboard) - @Input() welcomeTitle: string @Input() welcomeSubTitle: string @Input() placeholder: string @@ -161,7 +157,6 @@ export class NgmCopilotChatComponent { @Input() user: IUser - @Output() copied = new EventEmitter() @Output() conversationsChange = new EventEmitter() @Output() enableCopilot = new EventEmitter() @@ -746,22 +741,6 @@ export class NgmCopilotChatComponent { this.#activatedPrompt.set(event.option?.value) } - copyMessage(message: CopilotChatMessage) { - this.copied.emit(message.content) - this.#clipboard.copy(message.content) - this.messageCopied.update((ids) => [...ids, message.id]) - setTimeout(() => { - this.messageCopied.update((ids) => ids.filter((id) => id !== message.id)) - }, 3000) - } - - onCopy(copyButton) { - copyButton.copied = true - setTimeout(() => { - copyButton.copied = false - }, 3000) - } - dropCopilot(event: CdkDragDrop) { if (this.copilotEngine) { this.copilotEngine.dropCopilot(event) @@ -820,11 +799,6 @@ export class NgmCopilotChatComponent { } } - async revert(command: CopilotCommand, message: CopilotChatMessage) { - await command.revert?.(message.historyCursor) - message.reverted = true - } - async continue(conversation: CopilotChatConversation) { await this.copilotEngine.continue(conversation) } @@ -833,7 +807,4 @@ export class NgmCopilotChatComponent { await this.copilotEngine.finish(conversation) } - onRouteChange(conversationId: string, event: string) { - this.copilotEngine.updateConversationState(conversationId, {instructions: event}) - } } diff --git a/packages/copilot-angular/src/lib/token/token.component.ts b/packages/copilot-angular/src/lib/chat/token/token.component.ts similarity index 55% rename from packages/copilot-angular/src/lib/token/token.component.ts rename to packages/copilot-angular/src/lib/chat/token/token.component.ts index 69f612ed2..d952c7d15 100644 --- a/packages/copilot-angular/src/lib/token/token.component.ts +++ b/packages/copilot-angular/src/lib/chat/token/token.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core' +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core' import { MatTooltipModule } from '@angular/material/tooltip' import { TranslateModule } from '@ngx-translate/core' @@ -11,9 +11,9 @@ import { TranslateModule } from '@ngx-translate/core' class="bg-neutral-100 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded dark:bg-neutral-700 dark:text-neutral-300" matTooltip="{{ 'PAC.Copilot.CharacterLength' | translate: { Default: 'Character length' } }}" > - - - {{ characterLength }} + + + {{ characterLength() }} `, styles: [ ` @@ -27,14 +27,10 @@ import { TranslateModule } from '@ngx-translate/core' class: 'ngm-copilot-token' } }) -export class CopilotChatTokenComponent implements OnChanges { - @Input() content: string | null +export class CopilotChatTokenComponent { + readonly content = input() - characterLength = 0 - - ngOnChanges({ content }: SimpleChanges): void { - if (content) { - this.characterLength = content.currentValue?.length - } - } + readonly characterLength = computed(() => { + return this.content()?.length ?? 0 + }) } diff --git a/packages/copilot-angular/src/lib/i18n/zhHans.ts b/packages/copilot-angular/src/lib/i18n/zhHans.ts index 2d3e38e24..1db431f2d 100644 --- a/packages/copilot-angular/src/lib/i18n/zhHans.ts +++ b/packages/copilot-angular/src/lib/i18n/zhHans.ts @@ -49,5 +49,9 @@ export const ZhHans = { Invoke: '调用', Instructions: '指令', Reasoning: '推理', + Name: '名称', + Args: '参数', + ToolsCall: '工具调用', + Route: '路由' } } diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index aec099aeb..c722098ff 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -1,7 +1,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop' import { Injectable, TemplateRef, computed, inject, signal } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages' +import { AIMessage, BaseMessage, HumanMessage, isAIMessage } from '@langchain/core/messages' import { StringOutputParser } from '@langchain/core/output_parsers' import { Runnable } from '@langchain/core/runnables' import { ToolInputParsingException } from '@langchain/core/tools' @@ -19,6 +19,7 @@ import { CopilotEngine, DefaultModel, getCommandPrompt, + MessageDataType, nanoid } from '@metad/copilot' import { TranslateService } from '@ngx-translate/core' @@ -702,11 +703,14 @@ export class NgmCopilotEngineService implements CopilotEngine { if (value.next === 'FINISH' || value.next === END) { end = true } else { - message.templateRef = this.routeTemplate + // message.templateRef = this.routeTemplate message.data = { - next: value.next, - instructions: value.instructions, - reasoning: value.reasoning + type: MessageDataType.Route, + data: { + next: value.next, + instructions: value.instructions, + reasoning: value.reasoning + } } content += `${key}` + @@ -720,11 +724,21 @@ export class NgmCopilotEngineService implements CopilotEngine { this.#translate.instant('Copilot.Reasoning', { Default: 'Reasoning' }) + `
: ${value.reasoning || ''}` } - } else if (value.messages && value.messages[0]?.content) { - if (this.verbose()) { - content += `${key}\n` + } else if (value.messages) { + const _message = value.messages[0] + if (isAIMessage(_message)) { + if (_message.tool_calls?.length > 0) { + message.data = { + type: MessageDataType.ToolsCall, + data: _message.tool_calls.map(({name, args, id}) => ({name, args: JSON.stringify(args), id})) + } + } else if (_message.content) { + if (this.verbose()) { + content += `${key}\n` + } + content += value.messages.map((m) => m.content).join('\n\n') + } } - content += value.messages.map((m) => m.content).join('\n\n') } } ) @@ -738,7 +752,8 @@ export class NgmCopilotEngineService implements CopilotEngine { } else { verboseContent = content } - + } + if (content || message.data) { this.upsertMessage({ ...message, id: assistantId, @@ -769,7 +784,7 @@ export class NgmCopilotEngineService implements CopilotEngine { })) const lastMessage = this.getMessage(assistantId) - if (lastMessage.content) { + if (lastMessage.content || lastMessage.data) { this.upsertMessage({ id: assistantId, role: CopilotChatMessageRoleEnum.Assistant, diff --git a/packages/copilot/src/lib/types/types.ts b/packages/copilot/src/lib/types/types.ts index 494fd94b1..958ce6817 100644 --- a/packages/copilot/src/lib/types/types.ts +++ b/packages/copilot/src/lib/types/types.ts @@ -1,6 +1,6 @@ import { BaseMessage, FunctionCall, OpenAIToolCall } from '@langchain/core/messages' -import { AiProvider } from './providers' import { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions' +import { AiProvider } from './providers' export const DefaultModel = 'gpt-3.5-turbo' export const DefaultBusinessRole = 'default' @@ -147,4 +147,9 @@ export type AIOptions = ChatCompletionCreateParamsBase & { useSystemPrompt?: boolean verbose?: boolean interactive?: boolean +} + +export enum MessageDataType { + Route = 'route', + ToolsCall = 'tools_call', } \ No newline at end of file From aaba7dc8a3dc249958566b0943955cad8414ca3f Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 24 Jul 2024 20:01:40 +0800 Subject: [PATCH 21/53] feat: dba command agent --- .../@core/copilot/example-vector-retriever.ts | 60 +++-- apps/cloud/src/app/@core/copilot/few-shot.ts | 4 +- apps/cloud/src/app/@core/copilot/index.ts | 1 + .../cloud/src/app/@core/copilot/references.ts | 19 ++ .../model/copilot/dba/command.ts | 29 +++ .../semantic-model/model/copilot/dba/graph.ts | 42 +++ .../semantic-model/model/copilot/dba/index.ts | 2 + .../model/copilot/dba/supervisor.ts | 32 +++ .../semantic-model/model/copilot/dba/tools.ts | 0 .../semantic-model/model/copilot/dba/types.ts | 3 + .../semantic-model/model/copilot/index.ts | 3 +- .../model/copilot/modeler/command.ts | 17 -- .../model/copilot/modeler/graph.ts | 43 +--- .../model/copilot/modeler/planner.ts | 240 +++++++++--------- .../model/copilot/modeler/supervisor.ts | 17 +- .../semantic-model/model/copilot/schema.ts | 2 +- .../model/copilot/table/command.ts | 14 +- .../model/copilot/table/graph.ts | 30 ++- .../model/copilot/table/tools.ts | 1 - .../model/copilot/table/types.ts | 6 + .../semantic-model/model/model.component.ts | 3 +- apps/cloud/src/assets/i18n/zh-Hans.json | 1 + .../copilot-angular/src/lib/i18n/zhHans.ts | 3 +- 23 files changed, 346 insertions(+), 226 deletions(-) create mode 100644 apps/cloud/src/app/@core/copilot/references.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/command.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/graph.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/index.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/supervisor.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/tools.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/dba/types.ts create mode 100644 apps/cloud/src/app/features/semantic-model/model/copilot/table/types.ts diff --git a/apps/cloud/src/app/@core/copilot/example-vector-retriever.ts b/apps/cloud/src/app/@core/copilot/example-vector-retriever.ts index db9eb2068..28fcdc323 100644 --- a/apps/cloud/src/app/@core/copilot/example-vector-retriever.ts +++ b/apps/cloud/src/app/@core/copilot/example-vector-retriever.ts @@ -8,9 +8,9 @@ import { VectorStoreRetrieverInterface, VectorStoreRetrieverMMRSearchKwargs } from '@langchain/core/vectorstores' -import { CopilotExampleService } from '../services/' import { catchError, firstValueFrom, of, timeout } from 'rxjs' import { SERVER_REQUEST_TIMEOUT } from '../config' +import { CopilotExampleService } from '../services/' /** * Type for options when adding a document to the VectorStore. @@ -22,7 +22,7 @@ type AddDocumentOptions = Record * Class for performing document retrieval from a VectorStore. Can perform * similarity search or maximal marginal relevance search. */ -export class VectorStoreRetriever +export class ExampleVectorStoreRetriever extends BaseRetriever implements VectorStoreRetrieverInterface { @@ -78,32 +78,40 @@ export class VectorStoreRetriever { - return of([]) - }) - )) + return await firstValueFrom( + this.service + .maxMarginalRelevanceSearch(query, { + k: this.k, + filter: this.filter, + command: this.command, + role: this.role(), + ...this.searchKwargs + }) + .pipe( + timeout(SERVER_REQUEST_TIMEOUT), + catchError((error) => { + return of([]) + }) + ) + ) } - return await firstValueFrom(this.service.similaritySearch(query, { - command: this.command, - k: this.k, - filter: this.filter, - role: this.role(), - score: this.score - }).pipe( - timeout(SERVER_REQUEST_TIMEOUT), - catchError((error) => { - return of([]) - }) - )) + return await firstValueFrom( + this.service + .similaritySearch(query, { + command: this.command, + k: this.k, + filter: this.filter, + role: this.role(), + score: this.score + }) + .pipe( + timeout(SERVER_REQUEST_TIMEOUT), + catchError((error) => { + return of([]) + }) + ) + ) } async addDocuments(documents: DocumentInterface[], options?: AddDocumentOptions): Promise { diff --git a/apps/cloud/src/app/@core/copilot/few-shot.ts b/apps/cloud/src/app/@core/copilot/few-shot.ts index 85d857bc4..bafa09ba3 100644 --- a/apps/cloud/src/app/@core/copilot/few-shot.ts +++ b/apps/cloud/src/app/@core/copilot/few-shot.ts @@ -3,7 +3,7 @@ import { SemanticSimilarityExampleSelector } from '@langchain/core/example_selec import { FewShotPromptTemplate, PromptTemplate } from '@langchain/core/prompts' import { ExampleVectorStoreRetrieverInput, NgmCommandFewShotPromptToken, NgmCopilotService } from '@metad/copilot-angular' import { CopilotExampleService } from '../services/copilot-example.service' -import { VectorStoreRetriever } from './example-vector-retriever' +import { ExampleVectorStoreRetriever } from './example-vector-retriever' export function injectAgentFewShotTemplate(command: string, fields?: ExampleVectorStoreRetrieverInput) { const copilotService = inject(NgmCopilotService) @@ -27,7 +27,7 @@ function createExampleFewShotPrompt( ) return new FewShotPromptTemplate({ exampleSelector: new SemanticSimilarityExampleSelector({ - vectorStoreRetriever: new VectorStoreRetriever( + vectorStoreRetriever: new ExampleVectorStoreRetriever( { ...(fields ?? { vectorStore: null }), vectorStore: null, diff --git a/apps/cloud/src/app/@core/copilot/index.ts b/apps/cloud/src/app/@core/copilot/index.ts index fd58255d5..0672977b2 100644 --- a/apps/cloud/src/app/@core/copilot/index.ts +++ b/apps/cloud/src/app/@core/copilot/index.ts @@ -2,5 +2,6 @@ export * from './dimension-member-retriever' export * from './example-vector-retriever' export * from './few-shot' export * from './checkpoint-saver' +export * from './references' export * as Route from './agent-route' export * as Plan from './agent-plan' \ No newline at end of file diff --git a/apps/cloud/src/app/@core/copilot/references.ts b/apps/cloud/src/app/@core/copilot/references.ts new file mode 100644 index 000000000..5179d7f1a --- /dev/null +++ b/apps/cloud/src/app/@core/copilot/references.ts @@ -0,0 +1,19 @@ +import { inject } from '@angular/core' +import { ExampleVectorStoreRetrieverInput, NgmCopilotService } from '@metad/copilot-angular' +import { CopilotExampleService } from '../services/copilot-example.service' +import { ExampleVectorStoreRetriever } from './example-vector-retriever' + +export function injectExampleReferencesRetriever(command: string, fields?: ExampleVectorStoreRetrieverInput) { + const copilotService = inject(NgmCopilotService) + const copilotExampleService = inject(CopilotExampleService) + + return new ExampleVectorStoreRetriever( + { + ...(fields ?? { vectorStore: null }), + vectorStore: null, + command, + role: copilotService.role + }, + copilotExampleService + ) +} diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/command.ts new file mode 100644 index 000000000..846a5da8d --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/command.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core' +import { CopilotAgentType } from '@metad/copilot' +import { injectCopilotCommand } from '@metad/copilot-angular' +import { TranslateService } from '@ngx-translate/core' +import { NGXLogger } from 'ngx-logger' +import { injectAgentFewShotTemplate } from '../../../../../@core/copilot' +import { injectDBACreator } from './graph' +import { TABLE_CREATOR_NAME } from '../table/types' + +export function injectDBACommand() { + const logger = inject(NGXLogger) + const translate = inject(TranslateService) + const createDBA = injectDBACreator() + + const commandName = 'dba' + const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) + return injectCopilotCommand(commandName, { + description: translate.instant('PAC.MODEL.Copilot.CommandDBADesc', { + Default: 'Describe the requirements for database management' + }), + agent: { + type: CopilotAgentType.Graph, + conversation: true, + interruptBefore: [TABLE_CREATOR_NAME] + }, + fewShotPrompt, + createGraph: createDBA + }) +} diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/graph.ts new file mode 100644 index 000000000..408b899f8 --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/graph.ts @@ -0,0 +1,42 @@ +import { RunnableLambda } from '@langchain/core/runnables' +import { START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web' +import { CreateGraphOptions, Team } from '@metad/copilot' +import { injectRunTableCreator } from '../table' +import { TABLE_CREATOR_NAME } from '../table/types' +import { createSupervisorAgent } from './supervisor' +import { DBAState } from './types' + +const superState: StateGraphArgs['channels'] = { + ...Team.createState() +} + +export function injectDBACreator() { + const createTableAgent = injectRunTableCreator() + + return async ({ llm }: CreateGraphOptions) => { + const tools = [] + const supervisorAgent = await createSupervisorAgent({ llm, tools }) + const tableAgent = await createTableAgent({ llm }) + + const superGraph = new StateGraph({ channels: superState }) + .addNode(Team.SUPERVISOR_NAME, supervisorAgent) + .addNode( + TABLE_CREATOR_NAME, + RunnableLambda.from(async (state: DBAState) => { + const { messages } = await tableAgent.invoke({ + input: state.instructions, + role: state.role, + context: state.context, + language: state.language, + messages: [] + }) + return Team.responseToolMessage(state.tool_call_id, messages) + }) + ) + .addEdge(TABLE_CREATOR_NAME, Team.SUPERVISOR_NAME) + .addConditionalEdges(Team.SUPERVISOR_NAME, Team.supervisorRouter) + .addEdge(START, Team.SUPERVISOR_NAME) + + return superGraph + } +} diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/index.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/index.ts new file mode 100644 index 000000000..af75548e3 --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/index.ts @@ -0,0 +1,2 @@ +export * from './graph' +export * from './command' diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/supervisor.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/supervisor.ts new file mode 100644 index 000000000..27b13945b --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/supervisor.ts @@ -0,0 +1,32 @@ +import { DynamicStructuredTool } from '@langchain/core/tools' +import { ChatOpenAI } from '@langchain/openai' +import { Team } from '@metad/copilot' +import { TABLE_CREATOR_NAME } from '../table/types' + +export async function createSupervisorAgent({ + llm, + tools, +}: { + llm: ChatOpenAI + tools: DynamicStructuredTool[] +}) { + const agent = await Team.createSupervisorAgent( + llm, + [ + { + name: TABLE_CREATOR_NAME, + description: 'Create a table, only one at a time' + } + ], + tools, + `You are a Database Administrator for data analysis, now you need create a plan for the final goal. +{role} +{language} +{context} +调用工具时请在参数中说明详细功能和用途。 +`, + `Use only one tool at a time` + ) + + return agent +} diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/tools.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/tools.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dba/types.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/types.ts new file mode 100644 index 000000000..0b0120097 --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dba/types.ts @@ -0,0 +1,3 @@ +import { Team } from '@metad/copilot' + +export interface DBAState extends Team.State {} diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/index.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/index.ts index 8d5ab8e13..2dc3f3765 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/index.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/index.ts @@ -6,4 +6,5 @@ export * from './calculation' export * from './query' export * from './modeler' export * from './context' -export * from './table' \ No newline at end of file +export * from './table' +export * from './dba' \ No newline at end of file diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts index 82cd1c82e..98bf1242f 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts @@ -8,30 +8,13 @@ import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME } from '../cube' import { DIMENSION_MODELER_NAME } from '../dimension' import { injectCreateModelerGraph } from './graph' -import { injectCreateModelerPlanner } from './planner' export function injectModelerCommand() { const logger = inject(NGXLogger) const translate = inject(TranslateService) const modelService = inject(SemanticModelService) - const createModelerPlanner = injectCreateModelerPlanner() - const createModelerGraph = injectCreateModelerGraph() - injectCopilotCommand('modeler-plan', { - hidden: true, - alias: 'mlp', - description: 'Plan command for semantic model', - agent: { - type: CopilotAgentType.Graph, - conversation: true, - interruptAfter: ['tools'] - }, - createGraph: async ({ llm }: CreateGraphOptions) => { - return await createModelerPlanner({ llm }) - } - }) - const commandName = 'modeler' const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) return injectCopilotCommand(commandName, { diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts index 63d398b03..c0032e0aa 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts @@ -7,9 +7,9 @@ import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME, injectRunCubeModeler } from '../cube' import { DIMENSION_MODELER_NAME, injectRunDimensionModeler } from '../dimension/' import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' -import { injectRunModelerPlanner } from './planner' import { createSupervisorAgent } from './supervisor' import { ModelerState } from './types' +import { injectExampleReferencesRetriever } from 'apps/cloud/src/app/@core/copilot' const superState: StateGraphArgs['channels'] = { ...Team.createState(), @@ -17,60 +17,25 @@ const superState: StateGraphArgs['channels'] = { export function injectCreateModelerGraph() { const modelService = inject(SemanticModelService) - const createModelerPlanner = injectRunModelerPlanner() const createDimensionModeler = injectRunDimensionModeler() const createCubeModeler = injectRunCubeModeler() const selectTablesTool = injectSelectTablesTool() const queryTablesTool = injectQueryTablesTool() + const referencesRetriever = injectExampleReferencesRetriever('modeler/references', {k: 3, vectorStore: null}) + const dimensions = modelService.dimensions return async ({ llm }: CreateGraphOptions) => { const tools = [selectTablesTool, queryTablesTool] - const supervisorAgent = await createSupervisorAgent({ llm, dimensions, tools }) + const supervisorAgent = await createSupervisorAgent({ llm, dimensions, tools, referencesRetriever }) const dimensionAgent = await createDimensionModeler({ llm }) const cubeAgent = await createCubeModeler({ llm }) - // const supervisorNode = await createSupervisor(llm, [ - // { - // name: PLANNER_NAME, - // description: 'Create a plan for modeling' - // }, - // { - // name: DIMENSION_MODELER_NAME, - // description: 'Create a dimension, only one at a time' - // }, - // { - // name: CUBE_MODELER_NAME, - // description: 'Create a cube, only one at a time' - // } - // ]) - const plannerAgent = await createModelerPlanner({ llm }) - const superGraph = new StateGraph({ channels: superState }) // Add steps nodes .addNode(Team.SUPERVISOR_NAME, supervisorAgent.withConfig({ runName: Team.SUPERVISOR_NAME })) .addNode(Team.TOOLS_NAME, new ToolNode(tools)) - // .addNode(SUPERVISOR_NAME, RunnableLambda.from(async (state: State) => { - // const _state = await supervisorNode.invoke(state) - // return { - // ..._state, - // messages: [ - // new HumanMessage(`Call ${_state.next} with instructions: ${_state.instructions}`) - // ] - // } - // })) - // .addNode(PLANNER_NAME, - // RunnableLambda.from(async (state: State) => { - // return plannerAgent.invoke({ - // input: state.instructions, - // role: state.role, - // context: state.context, - // language: state.language, - // messages: [] - // }) - // }) - // ) .addNode( DIMENSION_MODELER_NAME, RunnableLambda.from(async (state: ModelerState) => { diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts index d794c8266..aadd2a211 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/planner.ts @@ -1,134 +1,134 @@ -import { inject } from '@angular/core' -import { HumanMessage, SystemMessage } from '@langchain/core/messages' -import { SystemMessagePromptTemplate } from '@langchain/core/prompts' -import { RunnableLambda } from '@langchain/core/runnables' -import { StateGraphArgs } from '@langchain/langgraph/web' -import { ChatOpenAI } from '@langchain/openai' -import { AgentState } from '@metad/copilot-angular' -import { - injectAgentFewShotTemplate -} from 'apps/cloud/src/app/@core/copilot' -import { SemanticModelService } from '../../model.service' -import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' -import { createCopilotAgentState, createReactAgent, Team } from '@metad/copilot' +// import { inject } from '@angular/core' +// import { HumanMessage, SystemMessage } from '@langchain/core/messages' +// import { SystemMessagePromptTemplate } from '@langchain/core/prompts' +// import { RunnableLambda } from '@langchain/core/runnables' +// import { StateGraphArgs } from '@langchain/langgraph/web' +// import { ChatOpenAI } from '@langchain/openai' +// import { AgentState } from '@metad/copilot-angular' +// import { +// injectAgentFewShotTemplate +// } from 'apps/cloud/src/app/@core/copilot' +// import { SemanticModelService } from '../../model.service' +// import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' +// import { createCopilotAgentState, createReactAgent, Team } from '@metad/copilot' -const SYSTEM_PROMPT = - `You are a cube modeler for data analysis, now you need create a plan for the final goal. -{role} -{language} -1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes. - Or use the 'selectTables' tool to get all tables then select the required physical tables from them. -2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. - If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. - If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. - If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions. - Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension. - For example, create a dimension called Department with the fields: First-level Department, Second-level Department, Third-level Department, and add First-level Department, Second-level Department, and Third-level Department as the levels of the dimension in order. - If you are creating a shared dimension, please provide your reason. -3. Distinguish whether each table is a dimension table or a fact table. If it is a dimension table, you need to create a shared dimension for it. If it is a fact table, create a cube and inline dimension or associate the created shared dimension. -4. Each step of the plan corresponds to one of the tasks 'Create a shared dimension' and 'Create a cube with inline dimensions and share dimensions'. +// const SYSTEM_PROMPT = +// `You are a cube modeler for data analysis, now you need create a plan for the final goal. +// {role} +// {language} +// 1. If user-provided tables, consider which of them are used to create shared dimensions and which are used to create cubes. +// Or use the 'selectTables' tool to get all tables then select the required physical tables from them. +// 2. The dimensions of a model are divided into two types: shared dimensions and inline dimensions. +// If a dimension has an independent dimension table, it can be created as a shared dimension. Otherwise, the dimension field in the fact table is created as an inline dimension. Shared dimensions are created from independent dimension physical tables. +// If the dimension fields of the model to be created are in the fact table, there is no need to create shared dimensions. +// If the dimension fields of the fact table need to be associated with independent dimension physical tables, you need to create shared dimensions for this dimension physical table or use existing shared dimensions. +// Create a dimension for fields that clearly belong to different levels of the same dimension, and add the fields from coarse to fine granularity as the levels of the dimension. +// For example, create a dimension called Department with the fields: First-level Department, Second-level Department, Third-level Department, and add First-level Department, Second-level Department, and Third-level Department as the levels of the dimension in order. +// If you are creating a shared dimension, please provide your reason. +// 3. Distinguish whether each table is a dimension table or a fact table. If it is a dimension table, you need to create a shared dimension for it. If it is a fact table, create a cube and inline dimension or associate the created shared dimension. +// 4. Each step of the plan corresponds to one of the tasks 'Create a shared dimension' and 'Create a cube with inline dimensions and share dimensions'. -A plan is an array of independent, ordered steps. -This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. -The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps. +// A plan is an array of independent, ordered steps. +// This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. +// The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps. -For example: - Objective is: Create a cube using the table. - Table context: - Table is: - - name: sales_data - caption: 销售数据 - columns: - - name: company_code - caption: 公司代码 - type: character varying - - name: product_code - caption: 产品代码 - type: character varying - - name: sales_amount - caption: 销售金额 - type: numeric +// For example: +// Objective is: Create a cube using the table. +// Table context: +// Table is: +// - name: sales_data +// caption: 销售数据 +// columns: +// - name: company_code +// caption: 公司代码 +// type: character varying +// - name: product_code +// caption: 产品代码 +// type: character varying +// - name: sales_amount +// caption: 销售金额 +// type: numeric - - name: product_data - caption: 产品数据 - columns: - - name: product_code - caption: 产品代码 - type: character varying - - name: product_name - caption: 产品名称 - type: character varying - - name: product_category - caption: 产品类别 - type: character varying -Answer is: Think about the shared dimension and inline dimension of the model: -The company_code field in the sales_data fact table, it do not have a dimension table, so it is a inline dimension. -The product_code field has a dimension table 'product_data', so it is a shared dimension. -The plan are as follows: - 1. Create a shared dimension 'Product' for table product_data. - 2. Create a cube with share dimension 'Product' and inline dimensions: 'Company' for table sales_data. +// - name: product_data +// caption: 产品数据 +// columns: +// - name: product_code +// caption: 产品代码 +// type: character varying +// - name: product_name +// caption: 产品名称 +// type: character varying +// - name: product_category +// caption: 产品类别 +// type: character varying +// Answer is: Think about the shared dimension and inline dimension of the model: +// The company_code field in the sales_data fact table, it do not have a dimension table, so it is a inline dimension. +// The product_code field has a dimension table 'product_data', so it is a shared dimension. +// The plan are as follows: +// 1. Create a shared dimension 'Product' for table product_data. +// 2. Create a cube with share dimension 'Product' and inline dimensions: 'Company' for table sales_data. -Table context: -{context} +// Table context: +// {context} -Avoid creating already existing shared dimensions: -{dimensions} -just use them directly in the cube creation task. -` +// Avoid creating already existing shared dimensions: +// {dimensions} +// just use them directly in the cube creation task. +// ` -export function injectCreateModelerPlanner() { - const modelService = inject(SemanticModelService) - const selectTablesTool = injectSelectTablesTool() - const queryTablesTool = injectQueryTablesTool() +// export function injectCreateModelerPlanner() { +// const modelService = inject(SemanticModelService) +// const selectTablesTool = injectSelectTablesTool() +// const queryTablesTool = injectQueryTablesTool() - const dimensions = modelService.dimensions +// const dimensions = modelService.dimensions - return async ({ llm }: { llm: ChatOpenAI }) => { - const tools = [selectTablesTool, queryTablesTool] +// return async ({ llm }: { llm: ChatOpenAI }) => { +// const tools = [selectTablesTool, queryTablesTool] - const systemContext = async () => { - return dimensions().length - ? `Existing shared dimensions:\n` + - dimensions() - .map((d) => `- name: ${d.name}\n caption: ${d.caption || ''}`) - .join('\n') - : `There are no existing shared dimensions.` - } +// const systemContext = async () => { +// return dimensions().length +// ? `Existing shared dimensions:\n` + +// dimensions() +// .map((d) => `- name: ${d.name}\n caption: ${d.caption || ''}`) +// .join('\n') +// : `There are no existing shared dimensions.` +// } - const state: StateGraphArgs['channels'] = createCopilotAgentState() - return createReactAgent({ - llm, - state, - tools, - messageModifier: async (state) => { - const system = await SystemMessagePromptTemplate.fromTemplate(SYSTEM_PROMPT).format({ - ...state, - dimensions: await systemContext() - }) - return [new SystemMessage(system), ...state.messages] - } - }) - } -} +// const state: StateGraphArgs['channels'] = createCopilotAgentState() +// return createReactAgent({ +// llm, +// state, +// tools, +// messageModifier: async (state) => { +// const system = await SystemMessagePromptTemplate.fromTemplate(SYSTEM_PROMPT).format({ +// ...state, +// dimensions: await systemContext() +// }) +// return [new SystemMessage(system), ...state.messages] +// } +// }) +// } +// } -export function injectRunModelerPlanner() { - const createModelerPlanner = injectCreateModelerPlanner() - const fewShotPrompt = injectAgentFewShotTemplate('modeler/planner', { k: 1, vectorStore: null }) +// export function injectRunModelerPlanner() { +// const createModelerPlanner = injectCreateModelerPlanner() +// const fewShotPrompt = injectAgentFewShotTemplate('modeler/planner', { k: 1, vectorStore: null }) - return async ({ llm }: { llm: ChatOpenAI }) => { - const agent = await createModelerPlanner({ llm }) +// return async ({ llm }: { llm: ChatOpenAI }) => { +// const agent = await createModelerPlanner({ llm }) - return RunnableLambda.from(async (state: AgentState) => { - const content = await fewShotPrompt.format({ input: state.input, context: '' }) - return { - input: state.input, - messages: [new HumanMessage(content)], - role: state.role, - context: state.context, - language: state.language, - } - }) - .pipe(agent) - .pipe(Team.joinGraph) - } -} +// return RunnableLambda.from(async (state: AgentState) => { +// const content = await fewShotPrompt.format({ input: state.input, context: '' }) +// return { +// input: state.input, +// messages: [new HumanMessage(content)], +// role: state.role, +// context: state.context, +// language: state.language, +// } +// }) +// .pipe(agent) +// .pipe(Team.joinGraph) +// } +// } diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts index 565ee73a7..a534db0fc 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/supervisor.ts @@ -8,15 +8,19 @@ import { CUBE_MODELER_NAME } from '../cube' import { DIMENSION_MODELER_NAME } from '../dimension' import { getTablesFromDimension } from '../types' import { ModelerState } from './types' +import { formatDocumentsAsString } from 'langchain/util/document' +import { VectorStoreRetriever } from '@langchain/core/vectorstores' export async function createSupervisorAgent({ llm, dimensions, - tools + tools, + referencesRetriever }: { llm: ChatOpenAI dimensions: Signal tools: DynamicStructuredTool[] + referencesRetriever: VectorStoreRetriever }) { const getDimensions = async () => { return dimensions().length @@ -99,16 +103,23 @@ The plan are as follows: Avoid creating already existing shared dimensions: {dimensions} just use them directly in the cube creation task. -Please plan the cube model first, and then decide to call route to create it step by step. +Please plan the cube modeling first, and then decide to call route to create it ont by one. + +Use the following pieces of context to answer the question at the end. +If you don't know the answer, just say that you don't know, don't try to make up an answer. +---------------- +{references} `, `Use only one tool at a time` ) return RunnableLambda.from(async (state: ModelerState) => { const dimensions = await getDimensions() + const references = await referencesRetriever.pipe(formatDocumentsAsString).invoke(state.input) return { ...state, - dimensions + dimensions, + references } }).pipe(agent) } diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/schema.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/schema.ts index 94ee0c08e..e02b984ae 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/schema.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/schema.ts @@ -35,7 +35,7 @@ const BaseHierarchySchema = { caption: z.string().describe('The caption of the level'), column: z.string().describe('The column of the level'), type: z - .enum(['String', 'Integer', 'Numeric', 'Boolean']) + .enum(['String', 'Integer', 'Numeric', 'Boolean', 'Date']) .optional() .describe('The type of the column, must be set if the column type is not string'), diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts index 18966e225..65d2f523d 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts @@ -4,28 +4,20 @@ import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' import { injectAgentFewShotTemplate } from '../../../../../@core/copilot' -import { SemanticModelService } from '../../model.service' import { injectTableCreator } from './graph' +import { TABLE_COMMAND_NAME } from './types' export function injectTableCommand() { const logger = inject(NGXLogger) const translate = inject(TranslateService) - const modelService = inject(SemanticModelService) const createTableCreator = injectTableCreator() - const commandName = 'table' - const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) - return injectCopilotCommand(commandName, { + const fewShotPrompt = injectAgentFewShotTemplate(TABLE_COMMAND_NAME, { k: 1, vectorStore: null }) + return injectCopilotCommand(TABLE_COMMAND_NAME, { alias: 't', description: translate.instant('PAC.MODEL.Copilot.CommandTableDesc', { Default: 'Descripe structure or business logic of the table' }), - historyCursor: () => { - return modelService.getHistoryCursor() - }, - revert: async (index: number) => { - modelService.gotoHistoryCursor(index) - }, agent: { type: CopilotAgentType.Graph, conversation: true, diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts index 4a12d0db9..75a7c6039 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/graph.ts @@ -1,13 +1,15 @@ import { inject } from '@angular/core' -import { SystemMessage } from '@langchain/core/messages' +import { HumanMessage, SystemMessage } from '@langchain/core/messages' import { SystemMessagePromptTemplate } from '@langchain/core/prompts' import { StateGraphArgs } from '@langchain/langgraph/web' -import { createCopilotAgentState, CreateGraphOptions, createReactAgent } from '@metad/copilot' +import { createCopilotAgentState, CreateGraphOptions, createReactAgent, Team } from '@metad/copilot' import { AgentState } from '@metad/copilot-angular' import { SemanticModelService } from '../../model.service' import { injectCreateTableTool } from './tools' +import { RunnableLambda } from '@langchain/core/runnables' +import { TABLE_COMMAND_NAME } from './types' +import { injectAgentFewShotTemplate } from 'apps/cloud/src/app/@core/copilot' -export const TABLE_CREATOR_NAME = 'TableCreator' function createSystemPrompt(dialect: string) { return ( @@ -44,3 +46,25 @@ export function injectTableCreator() { }) } } + +export function injectRunTableCreator() { + const createTableCreator = injectTableCreator() + const fewShotPrompt = injectAgentFewShotTemplate(TABLE_COMMAND_NAME, { k: 1, vectorStore: null }) + + return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { + const agent = await createTableCreator({ llm, checkpointer, interruptBefore, interruptAfter }) + + return RunnableLambda.from(async (state: AgentState) => { + const content = await fewShotPrompt.format({ input: state.input, context: '' }) + return { + input: state.input, + messages: [new HumanMessage(content)], + role: state.role, + context: state.context, + language: state.language + } + }) + .pipe(agent) + .pipe(Team.joinGraph) + } +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts index 85df168ff..77d6f264b 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/tools.ts @@ -4,7 +4,6 @@ import { NGXLogger } from 'ngx-logger' import { firstValueFrom } from 'rxjs' import { z } from 'zod' import { SemanticModelService } from '../../model.service' -import { ModelComponent } from '../../model.component' export function injectCreateTableTool() { const logger = inject(NGXLogger) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/types.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/types.ts new file mode 100644 index 000000000..c211ea8e7 --- /dev/null +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/types.ts @@ -0,0 +1,6 @@ +import { Team } from "@metad/copilot"; + +export const TABLE_COMMAND_NAME = 'table' +export const TABLE_CREATOR_NAME = 'TableCreator' +export interface TableState extends Team.State { +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/semantic-model/model/model.component.ts b/apps/cloud/src/app/features/semantic-model/model/model.component.ts index f12238416..bb390ea3a 100644 --- a/apps/cloud/src/app/features/semantic-model/model/model.component.ts +++ b/apps/cloud/src/app/features/semantic-model/model/model.component.ts @@ -50,7 +50,7 @@ import { TranslationBaseComponent } from '../../../@shared' import { AppService } from '../../../app.service' import { exportSemanticModel } from '../types' import { ModelUploadComponent } from '../upload/upload.component' -import { injectCubeCommand, injectDimensionCommand, injectModelerCommand, injectTableCommand, provideCopilotTables } from './copilot' +import { injectCubeCommand, injectDBACommand, injectDimensionCommand, injectModelerCommand, injectTableCommand, provideCopilotTables } from './copilot' import { CreateEntityDialogDataType, CreateEntityDialogRetType, @@ -215,6 +215,7 @@ export class ModelComponent extends TranslationBaseComponent implements IsDirty */ #cubeCommand = injectCubeCommand(this.dimensions) #dimensionCommand = injectDimensionCommand(this.dimensions) + #dbaCommand = injectDBACommand() #tableCommand = injectTableCommand() #entityDropAction = provideCopilotDropAction({ id: CdkDragDropContainers.Tables, diff --git a/apps/cloud/src/assets/i18n/zh-Hans.json b/apps/cloud/src/assets/i18n/zh-Hans.json index 8f5f1b9c0..dfcf9ced6 100644 --- a/apps/cloud/src/assets/i18n/zh-Hans.json +++ b/apps/cloud/src/assets/i18n/zh-Hans.json @@ -1013,6 +1013,7 @@ "CommandTableDesc": "描述表的结构或业务逻辑", "CommandCalculatedDesc": "描述计算度量的业务逻辑", "CommandFormulaDesc": "描述计算度量的公式逻辑", + "CommandDBADesc": "描述数据库管理的需求", "Examples": { "CreateCubeByTableInfo": "根据表信息创建立方体 Cube", "CreateDimensionByTableInfo": "根据表信息创建维度 Dimension", diff --git a/packages/copilot-angular/src/lib/i18n/zhHans.ts b/packages/copilot-angular/src/lib/i18n/zhHans.ts index 1db431f2d..75d5654c3 100644 --- a/packages/copilot-angular/src/lib/i18n/zhHans.ts +++ b/packages/copilot-angular/src/lib/i18n/zhHans.ts @@ -52,6 +52,7 @@ export const ZhHans = { Name: '名称', Args: '参数', ToolsCall: '工具调用', - Route: '路由' + Route: '路由', + RecursionLimit: '递归限制' } } From 9fc83f2fc305a61d327f63eaf31b7d8c0cf11de9 Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 24 Jul 2024 20:02:13 +0800 Subject: [PATCH 22/53] feat: enable recursion limit option --- .../entity/entity-schema/_entity-schema-theme.scss | 7 +++++-- .../entity-schema/entity-schema.component.html | 4 ++-- .../lib/chat/ai-message/ai-message.component.scss | 2 +- .../src/lib/chat/chat.component.html | 8 ++++---- .../copilot-angular/src/lib/chat/chat.component.ts | 13 +++++++------ .../src/lib/services/engine.service.ts | 4 ++-- packages/copilot/src/lib/types/types.ts | 1 + 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/angular/entity/entity-schema/_entity-schema-theme.scss b/packages/angular/entity/entity-schema/_entity-schema-theme.scss index 2aa913d15..f0c678d73 100644 --- a/packages/angular/entity/entity-schema/_entity-schema-theme.scss +++ b/packages/angular/entity/entity-schema/_entity-schema-theme.scss @@ -28,7 +28,7 @@ .ngm-entity-schema__type { display: inline-block; white-space: nowrap; - width: 16px; + // width: 16px; height: 16px; margin-right: 5px; font-size: 12px; @@ -38,7 +38,10 @@ background-color: color.change(mat.get-color-from-palette($accent), $alpha: 0.1); border-radius: 3px; padding: 0 3px; - transform: scale(.7); + max-width: 60px; + overflow: hidden; + text-overflow: ellipsis; + // transform: scale(.7); } } diff --git a/packages/angular/entity/entity-schema/entity-schema.component.html b/packages/angular/entity/entity-schema/entity-schema.component.html index bd3fea800..304152514 100644 --- a/packages/angular/entity/entity-schema/entity-schema.component.html +++ b/packages/angular/entity/entity-schema/entity-schema.component.html @@ -16,7 +16,7 @@ } -
{{node.item.type?.[0]}}
+
{{node.item.dataType || node.item.type?.[0]}}
@@ -37,7 +37,7 @@ -
{{node.item.type?.[0]}}
+
{{node.item.dataType || node.item.type?.[0]}}
diff --git a/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss index f0dab0e8e..e7fed87a1 100644 --- a/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss +++ b/packages/copilot-angular/src/lib/chat/ai-message/ai-message.component.scss @@ -1,5 +1,5 @@ .ngm-copilot-ai-message__table { - @apply m-2 rounded-lg border border-slate-100 dark:border-slate-700; + @apply my-2 rounded-lg border border-slate-100 dark:border-slate-700; table { @apply border-collapse; diff --git a/packages/copilot-angular/src/lib/chat/chat.component.html b/packages/copilot-angular/src/lib/chat/chat.component.html index 65a7beed7..8f5247dde 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.html +++ b/packages/copilot-angular/src/lib/chat/chat.component.html @@ -51,7 +51,7 @@ } - - - - + + + diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index e92c3ee15..cea40fc8c 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -77,7 +77,7 @@ import { provideFadeAnimation } from '../core/animations' import { NgmHighlightDirective } from '../core/directives' import { NgmCopilotEnableComponent } from '../enable/enable.component' import { injectCommonCommands } from '../hooks/common' -import { NgmCopilotEngineService, NgmCopilotService } from '../services/' +import { AgentRecursionLimit, NgmCopilotEngineService, NgmCopilotService } from '../services/' import { CopilotChatTokenComponent } from './token/token.component' import { IUser, NgmCopilotChatMessage } from '../types' import { PlaceholderMessages } from './types' @@ -132,6 +132,7 @@ export class NgmCopilotChatComponent { NgxPopperjsPlacements = NgxPopperjsPlacements NgxPopperjsTriggers = NgxPopperjsTriggers CopilotChatMessageRoleEnum = CopilotChatMessageRoleEnum + AgentRecursionLimit = AgentRecursionLimit readonly _snackBar = inject(MatSnackBar) private copilotService = inject(NgmCopilotService) @@ -216,14 +217,14 @@ export class NgmCopilotChatComponent { this.openaiOptions.temperature = value } } - get n() { - return this.aiOptions.n + get recursionLimit() { + return this.aiOptions.recursionLimit } - set n(value) { + set recursionLimit(value) { if (this.copilotEngine) { - this.copilotEngine.aiOptions = { ...this.aiOptions, n: value } + this.copilotEngine.aiOptions = { ...this.aiOptions, recursionLimit: value } } else { - this.openaiOptions.n = value + this.openaiOptions.recursionLimit = value } } diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index c722098ff..436b7cc1a 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -676,7 +676,7 @@ export class NgmCopilotEngineService implements CopilotEngine { configurable: { thread_id: conversation.id }, - recursionLimit: AgentRecursionLimit + recursionLimit: this.aiOptions.recursionLimit ?? AgentRecursionLimit } ) @@ -685,7 +685,7 @@ export class NgmCopilotEngineService implements CopilotEngine { try { for await (const output of streamResults) { if (!output?.__end__) { - const message = {templateRef: null} as NgmCopilotChatMessage + const message = {data: null} as NgmCopilotChatMessage let content = '' Object.entries(output).forEach( ([key, value]: [ diff --git a/packages/copilot/src/lib/types/types.ts b/packages/copilot/src/lib/types/types.ts index 958ce6817..9f64b9c43 100644 --- a/packages/copilot/src/lib/types/types.ts +++ b/packages/copilot/src/lib/types/types.ts @@ -147,6 +147,7 @@ export type AIOptions = ChatCompletionCreateParamsBase & { useSystemPrompt?: boolean verbose?: boolean interactive?: boolean + recursionLimit?: number } export enum MessageDataType { From e7f2ed702b057d29fb8b3923b1379ab11f442810 Mon Sep 17 00:00:00 2001 From: meta-d Date: Wed, 24 Jul 2024 21:48:38 +0800 Subject: [PATCH 23/53] feat: suggestion --- .../model/copilot/modeler/command.ts | 18 ++++++++ .../src/lib/chat/chat.component.ts | 25 ++++------- .../src/lib/services/engine.service.ts | 43 ++++++++++++++----- packages/copilot/src/lib/command.ts | 20 ++++----- packages/copilot/src/lib/engine.ts | 2 +- 5 files changed, 71 insertions(+), 37 deletions(-) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts index 98bf1242f..014805544 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts @@ -8,6 +8,18 @@ import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME } from '../cube' import { DIMENSION_MODELER_NAME } from '../dimension' import { injectCreateModelerGraph } from './graph' +import { + ChatPromptTemplate, + PromptTemplate, + SystemMessagePromptTemplate, + AIMessagePromptTemplate, + HumanMessagePromptTemplate, +} from "@langchain/core/prompts"; +import { + AIMessage, + HumanMessage, + SystemMessage, +} from "@langchain/core/messages"; export function injectModelerCommand() { const logger = inject(NGXLogger) @@ -38,6 +50,12 @@ export function injectModelerCommand() { return await createModelerGraph({ llm }) + }, + suggestion: { + promptTemplate: ChatPromptTemplate.fromMessages([ + ["system", `用简短的一句话补全用户可能的提问,直接输出答案不要解释`], + ["human", '{input}'], + ]) } }) } diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index cea40fc8c..d302be782 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -392,20 +392,10 @@ export class NgmCopilotChatComponent { .filter((c) => !c.hidden) .sort((a, b) => (a.name > b.name ? 1 : a.name === b.name ? 0 : -1)) .forEach((command) => { - if (command.examples?.length) { - command.examples.forEach((example) => { - commands.push({ - ...command, - prompt: `/${command.name} ${example}`, - example - }) - }) - } else { - commands.push({ - ...command, - prompt: `/${command.name} ${command.description}` - }) - } + commands.push({ + ...command, + prompt: `/${command.name} ${command.description}` + }) }) return commands } @@ -462,13 +452,16 @@ export class NgmCopilotChatComponent { const commandWithContext = this.commandWithContext() return onlyCommand ? of(command?.description) - : command?.suggestionTemplate + : command?.suggestion ? this.copilotEngine.executeCommandSuggestion(prompt, { ...commandWithContext }) : of(null) }), catchError(() => of(null)) ) - .subscribe((text) => this.promptCompletion.set(text)) + .subscribe((output: any) => { + console.log(output) + this.promptCompletion.set(output.input) + }) constructor() { effect( diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index 436b7cc1a..366f7e964 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -22,6 +22,8 @@ import { MessageDataType, nanoid } from '@metad/copilot' +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { TranslateService } from '@ngx-translate/core' import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents' import { compact, flatten } from 'lodash-es' @@ -30,6 +32,7 @@ import { DropAction, NgmCopilotChatMessage } from '../types' import { injectCreateChatAgent } from './agent-free' import { NgmCopilotContextToken, recognizeContext, recognizeContextParams } from './context.service' import { NgmCopilotService } from './copilot.service' +import { JsonOutputFunctionsParser } from 'langchain/output_parsers' export const AgentRecursionLimit = 20 @@ -1057,25 +1060,36 @@ export class NgmCopilotEngineService implements CopilotEngine { async executeCommandSuggestion( input: string, options: { command: CopilotCommand; context: CopilotContext; signal?: AbortSignal } - ): Promise { + ): Promise { const { command, context, signal } = options // Context content const contextContent = context ? await recognizeContext(input, context) : null const params = await recognizeContextParams(input, context) - let systemPrompt = '' - try { - // Get System prompt - if (command.systemPrompt) { - systemPrompt = await command.systemPrompt({ params }) - } + if (command.fewShotPrompt) { + input = await command.fewShotPrompt.format({ input }) + } + const outputFunctions = { + functions: [ + { + name: "output_formatter", + description: "Should always be used to properly format output", + parameters: zodToJsonSchema(zodSchema), + }, + ], + function_call: { name: "output_formatter" }, + } + + const outputParser = new JsonOutputFunctionsParser(); + try { const llm = this.llm() + const secondaryLLM = this.secondaryLLM() const verbose = this.verbose() if (llm) { - if (command.suggestionTemplate) { - const chain = command.suggestionTemplate.pipe(this.llm()).pipe(new StringOutputParser()) - return await chain.invoke({ input, system_prompt: systemPrompt, context: contextContent, signal, verbose }) + if (command.suggestion?.promptTemplate) { + const chain = command.suggestion.promptTemplate.pipe((secondaryLLM ?? llm).bind(outputFunctions)).pipe(outputParser) + return await chain.invoke({ input, context: contextContent, signal, verbose }) } else { throw new Error('No completion template found') } @@ -1087,3 +1101,12 @@ export class NgmCopilotEngineService implements CopilotEngine { } } } + +const zodSchema = z.object({ + input: z.string().describe("Prompt after completion"), + suggestions: z + .array( + z.string().describe("One suggestion input"), + ) + .describe("An array of suggestions"), +}); \ No newline at end of file diff --git a/packages/copilot/src/lib/command.ts b/packages/copilot/src/lib/command.ts index 68346ac5c..55c496850 100644 --- a/packages/copilot/src/lib/command.ts +++ b/packages/copilot/src/lib/command.ts @@ -26,18 +26,18 @@ export interface CopilotCommand { * Description of the command */ description: string + // /** + // * @deprecated use suggestions + // * + // * Examples of the command usage + // */ + // examples?: string[] /** - * Examples of the command usage + * Input suggestions */ - examples?: string[] - /** - * The ai tools for input suggestion generation - */ - suggestionTools?: Array - /** - * The prompt template for input suggestion - */ - suggestionTemplate?: ChatPromptTemplate + suggestion?: { + promptTemplate: ChatPromptTemplate + } /** * @deprecated use prompt only */ diff --git a/packages/copilot/src/lib/engine.ts b/packages/copilot/src/lib/engine.ts index 6204a6b9b..cb86cf01e 100644 --- a/packages/copilot/src/lib/engine.ts +++ b/packages/copilot/src/lib/engine.ts @@ -166,5 +166,5 @@ export interface CopilotEngine { * @param input * @param options */ - executeCommandSuggestion(input: string, options: {command: CopilotCommand; context: CopilotContext}): Promise + executeCommandSuggestion(input: string, options: {command: CopilotCommand; context: CopilotContext}): Promise } From 6e95931871069272cb49ed4eaebd75585dc74648 Mon Sep 17 00:00:00 2001 From: meta-d Date: Thu, 25 Jul 2024 11:23:00 +0800 Subject: [PATCH 24/53] feat: remove ai & upgrade langchain --- package.json | 15 +- .../src/lib/chat/chat.component.ts | 12 +- .../src/lib/services/engine.service.ts | 33 +- packages/copilot/package.json | 1 + .../src/lib/chat_models/chat-ollama.ts | 22 + packages/copilot/src/lib/copilot.ts | 4 +- .../src/lib/dashscope/call-chat-api.ts | 173 ----- .../src/lib/dashscope/call-completion-api.ts | 141 ---- packages/copilot/src/lib/dashscope/index.ts | 2 - packages/copilot/src/lib/engine.ts | 41 +- .../src/lib/graph/react_agent_executor.ts | 5 +- .../copilot/src/lib/shared/call-chat-api.ts | 187 ----- .../src/lib/shared/call-completion-api.ts | 144 ---- packages/copilot/src/lib/shared/functions.ts | 107 --- .../src/lib/shared/parse-complex-response.ts | 118 ---- .../src/lib/shared/process-chat-stream.ts | 255 ------- .../src/lib/shared/read-data-stream.ts | 72 -- .../copilot/src/lib/shared/stream-parts.ts | 354 ---------- .../src/lib/streams/dashscope-stream.ts | 643 ------------------ packages/copilot/src/lib/streams/index.ts | 1 - packages/copilot/src/lib/types/providers.ts | 8 +- 21 files changed, 88 insertions(+), 2250 deletions(-) create mode 100644 packages/copilot/src/lib/chat_models/chat-ollama.ts delete mode 100644 packages/copilot/src/lib/dashscope/call-chat-api.ts delete mode 100644 packages/copilot/src/lib/dashscope/call-completion-api.ts delete mode 100644 packages/copilot/src/lib/dashscope/index.ts delete mode 100644 packages/copilot/src/lib/shared/call-chat-api.ts delete mode 100644 packages/copilot/src/lib/shared/call-completion-api.ts delete mode 100644 packages/copilot/src/lib/shared/functions.ts delete mode 100644 packages/copilot/src/lib/shared/parse-complex-response.ts delete mode 100644 packages/copilot/src/lib/shared/process-chat-stream.ts delete mode 100644 packages/copilot/src/lib/shared/read-data-stream.ts delete mode 100644 packages/copilot/src/lib/shared/stream-parts.ts delete mode 100644 packages/copilot/src/lib/streams/dashscope-stream.ts delete mode 100644 packages/copilot/src/lib/streams/index.ts diff --git a/package.json b/package.json index df759de80..0805c471b 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,11 @@ "@casl/angular": "^6.0.0", "@datorama/akita": "^6.2.3", "@duckdb/duckdb-wasm": "1.25.0", - "@langchain/community": "0.2.11", - "@langchain/core": "0.2.7", - "@langchain/langgraph": "0.0.26", - "@langchain/openai": "0.1.3", + "@langchain/community": "0.2.20", + "@langchain/core": "0.2.18", + "@langchain/langgraph": "0.0.31", + "@langchain/ollama": "^0.0.2", + "@langchain/openai": "0.2.4", "@microsoft/fetch-event-source": "^2.0.1", "@ng-matero/extensions": "^13.1.0", "@ng-web-apis/common": "^2.0.1", @@ -106,7 +107,6 @@ "@sentry/tracing": "^7.38.0", "@swc/helpers": "0.5.3", "@tinymce/tinymce-angular": "^6.0.1", - "ai": "^2.2.30", "angular-gridster2": "^14.0.1", "apache-arrow": "^9.0.0", "axios": "1.6.8", @@ -128,7 +128,7 @@ "immer": "^10.0.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", - "langchain": "0.2.5", + "langchain": "0.2.10", "lato-font": "^3.0.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -145,6 +145,7 @@ "ngx-quill": "^16.1.2", "ngxtension": "^3.1.2", "noto-serif-sc": "^8.0.0", + "ollama": "^0.5.6", "openai": "^4.6.0", "passport-dingtalk2": "^2.1.1", "pg": "^8.7.3", @@ -239,7 +240,7 @@ "postcss-import": "14.1.0", "postcss-preset-env": "7.5.0", "postcss-url": "10.1.3", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "rimraf": "^5.0.5", "tailwindcss": "3.3.5", "ts-jest": "29.1.1", diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index d302be782..d7846176a 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -48,6 +48,7 @@ import { CopilotChatMessageRoleEnum, CopilotCommand, CopilotContextItem, + SuggestionOutput, nanoid, } from '@metad/copilot' import { TranslateModule } from '@ngx-translate/core' @@ -458,9 +459,14 @@ export class NgmCopilotChatComponent { }), catchError(() => of(null)) ) - .subscribe((output: any) => { - console.log(output) - this.promptCompletion.set(output.input) + .subscribe((output: SuggestionOutput) => { + if (typeof output === 'string') { + this.promptCompletion.set(output) + } else if (output) { + this.promptCompletion.set(output.input) + } else { + this.promptCompletion.set('') + } }) constructor() { diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index 366f7e964..3ef79f654 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -20,10 +20,10 @@ import { DefaultModel, getCommandPrompt, MessageDataType, - nanoid + nanoid, + SuggestionOutput, + SuggestionOutputTool } from '@metad/copilot' -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; import { TranslateService } from '@ngx-translate/core' import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents' import { compact, flatten } from 'lodash-es' @@ -34,6 +34,7 @@ import { NgmCopilotContextToken, recognizeContext, recognizeContextParams } from import { NgmCopilotService } from './copilot.service' import { JsonOutputFunctionsParser } from 'langchain/output_parsers' + export const AgentRecursionLimit = 20 let uniqueId = 0 @@ -1060,7 +1061,7 @@ export class NgmCopilotEngineService implements CopilotEngine { async executeCommandSuggestion( input: string, options: { command: CopilotCommand; context: CopilotContext; signal?: AbortSignal } - ): Promise { + ): Promise { const { command, context, signal } = options // Context content const contextContent = context ? await recognizeContext(input, context) : null @@ -1070,25 +1071,14 @@ export class NgmCopilotEngineService implements CopilotEngine { input = await command.fewShotPrompt.format({ input }) } - const outputFunctions = { - functions: [ - { - name: "output_formatter", - description: "Should always be used to properly format output", - parameters: zodToJsonSchema(zodSchema), - }, - ], - function_call: { name: "output_formatter" }, - } - - const outputParser = new JsonOutputFunctionsParser(); try { const llm = this.llm() const secondaryLLM = this.secondaryLLM() const verbose = this.verbose() if (llm) { if (command.suggestion?.promptTemplate) { - const chain = command.suggestion.promptTemplate.pipe((secondaryLLM ?? llm).bind(outputFunctions)).pipe(outputParser) + const chain = command.suggestion.promptTemplate.pipe((secondaryLLM ?? llm).bindTools([SuggestionOutputTool])) + .pipe(new StringOutputParser()) return await chain.invoke({ input, context: contextContent, signal, verbose }) } else { throw new Error('No completion template found') @@ -1101,12 +1091,3 @@ export class NgmCopilotEngineService implements CopilotEngine { } } } - -const zodSchema = z.object({ - input: z.string().describe("Prompt after completion"), - suggestions: z - .array( - z.string().describe("One suggestion input"), - ) - .describe("An array of suggestions"), -}); \ No newline at end of file diff --git a/packages/copilot/package.json b/packages/copilot/package.json index 63d72c7fd..e03fc6c04 100644 --- a/packages/copilot/package.json +++ b/packages/copilot/package.json @@ -5,6 +5,7 @@ "@langchain/core": "0.2.5", "ai": "^2.2.30", "nanoid": "^4.0.2", + "ollama": "^0.5.6", "openai": "^4.6.0" } } diff --git a/packages/copilot/src/lib/chat_models/chat-ollama.ts b/packages/copilot/src/lib/chat_models/chat-ollama.ts new file mode 100644 index 000000000..4b6c442ed --- /dev/null +++ b/packages/copilot/src/lib/chat_models/chat-ollama.ts @@ -0,0 +1,22 @@ +import { ChatOllama, ChatOllamaInput } from '@langchain/ollama' +import { Ollama } from 'ollama/dist/browser' + +export class NgmChatOllama extends ChatOllama { + constructor(fields?: ChatOllamaInput & { headers: { [x: string]: string } }) { + super(fields ?? {}) + + this.client = new Ollama({ + host: fields?.baseUrl, + // For add custom headers (server token) + fetch: async (url: string | RequestInfo | URL, options: RequestInit) => { + return fetch(url, { + ...options, + headers: { + ...options.headers, + ...fields.headers + } + }) + } + }) + } +} diff --git a/packages/copilot/src/lib/copilot.ts b/packages/copilot/src/lib/copilot.ts index dc55c0bba..ff00be9d7 100644 --- a/packages/copilot/src/lib/copilot.ts +++ b/packages/copilot/src/lib/copilot.ts @@ -1,9 +1,9 @@ -import { ChatOllama } from '@langchain/community/chat_models/ollama' import { BaseChatModel } from '@langchain/core/language_models/chat_models' import { ChatOpenAI, ClientOptions } from '@langchain/openai' import { BehaviorSubject, catchError, combineLatest, map, of, shareReplay, switchMap } from 'rxjs' import { fromFetch } from 'rxjs/fetch' import { AI_PROVIDERS, AiProvider, BusinessRoleType, ICopilot } from './types' +import { NgmChatOllama } from './chat_models/chat-ollama' function modelsUrl(copilot: ICopilot) { const apiHost: string = copilot.apiHost || AI_PROVIDERS[copilot.provider]?.apiHost @@ -118,7 +118,7 @@ function createLLM(copilot: ICopilot, clientOpti temperature: 0 }) as T case AiProvider.Ollama: - return new ChatOllama({ + return new NgmChatOllama({ baseUrl: copilot.apiHost || null, model: copilot.defaultModel, headers: { diff --git a/packages/copilot/src/lib/dashscope/call-chat-api.ts b/packages/copilot/src/lib/dashscope/call-chat-api.ts deleted file mode 100644 index de2f80540..000000000 --- a/packages/copilot/src/lib/dashscope/call-chat-api.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { parseComplexResponse } from '../shared/parse-complex-response'; -import { - ChatRequest, - FunctionCall, - IdGenerator, - JSONValue, - Message, - ToolCall, -} from 'ai'; -import { COMPLEX_HEADER, createChunkDecoder } from 'ai'; -import { DashScopeStream } from '../streams'; - -export async function callChatApi({ - api, - model, - chatRequest, - messages, - body, - credentials, - headers, - abortController, - appendMessage, - restoreMessagesOnFailure, - onResponse, - onUpdate, - onFinish, - generateId, -}: { - api: string; - chatRequest: ChatRequest; - model: string; - messages: Omit[]; - body: Record; - credentials?: RequestCredentials; - headers?: HeadersInit; - abortController?: () => AbortController | null; - restoreMessagesOnFailure: () => void; - appendMessage: (message: Message) => void; - onResponse?: (response: Response) => void | Promise; - onUpdate: (merged: Message[], data: JSONValue[] | undefined) => void; - onFinish?: (message: Message) => void; - generateId: IdGenerator; -}): Promise { - const { functions, function_call } = chatRequest - - const response = await fetch(api, { - method: 'POST', - body: JSON.stringify({ - input: { - result_format: 'message', - messages, - functions, - function_call, - }, - parameters: { - }, - model - }), - headers: { - ...headers, - 'content-type': 'application/json', - accept: 'text/event-stream' - }, - signal: abortController?.()?.signal, - credentials, - }).catch(err => { - restoreMessagesOnFailure(); - throw err; - }); - - if (onResponse) { - try { - await onResponse(response); - } catch (err) { - throw err; - } - } - - if (!response.ok) { - restoreMessagesOnFailure(); - throw new Error( - (await response.text()) || 'Failed to fetch the chat response.', - ); - } - - if (!response.body) { - throw new Error('The response body is empty.'); - } - - const reader = DashScopeStream(response).getReader(); - const isComplexMode = response.headers.get(COMPLEX_HEADER) === 'true'; - - if (isComplexMode) { - return await parseComplexResponse({ - reader, - abortControllerRef: - abortController != null ? { current: abortController() } : undefined, - update: onUpdate, - onFinish(prefixMap) { - if (onFinish && prefixMap.text != null) { - onFinish(prefixMap.text); - } - }, - generateId, - }); - } else { - const createdAt = new Date(); - const decode = createChunkDecoder(false); - - // TODO-STREAMDATA: Remove this once Stream Data is not experimental - let streamedResponse = ''; - const replyId = generateId(); - const responseMessage: Message = { - id: replyId, - createdAt, - content: '', - role: 'assistant', - }; - - // TODO-STREAMDATA: Remove this once Stream Data is not experimental - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - // Update the chat state with the new message tokens. - streamedResponse = decode(value); - - if (streamedResponse.startsWith('{"function_call":')) { - // While the function call is streaming, it will be a string. - responseMessage['function_call'] = streamedResponse; - } else if (streamedResponse.startsWith('{"tool_calls":')) { - // While the tool calls are streaming, it will be a string. - responseMessage['tool_calls'] = streamedResponse; - } else { - responseMessage['content'] = streamedResponse; - } - - appendMessage({ ...responseMessage }); - - // The request has been aborted, stop reading the stream. - if (abortController?.() === null) { - reader.cancel(); - break; - } - } - - if (streamedResponse.startsWith('{"function_call":')) { - // Once the stream is complete, the function call is parsed into an object. - const parsedFunctionCall: FunctionCall = - JSON.parse(streamedResponse).function_call; - - responseMessage['function_call'] = parsedFunctionCall; - - appendMessage({ ...responseMessage }); - } - if (streamedResponse.startsWith('{"tool_calls":')) { - // Once the stream is complete, the tool calls are parsed into an array. - const parsedToolCalls: ToolCall[] = - JSON.parse(streamedResponse).tool_calls; - - responseMessage['tool_calls'] = parsedToolCalls; - - appendMessage({ ...responseMessage }); - } - - if (onFinish) { - onFinish(responseMessage); - } - - return responseMessage; - } -} diff --git a/packages/copilot/src/lib/dashscope/call-completion-api.ts b/packages/copilot/src/lib/dashscope/call-completion-api.ts deleted file mode 100644 index 3fbc39522..000000000 --- a/packages/copilot/src/lib/dashscope/call-completion-api.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { readDataStream } from '../shared/read-data-stream'; -import { JSONValue } from 'ai'; -import { COMPLEX_HEADER, createChunkDecoder } from 'ai'; - -export async function callCompletionApi({ - api, - prompt, - credentials, - headers, - body, - setCompletion, - setLoading, - setError, - setAbortController, - onResponse, - onFinish, - onError, - onData, -}: { - api: string; - prompt: string; - credentials?: RequestCredentials; - headers?: HeadersInit; - body: Record; - setCompletion: (completion: string) => void; - setLoading: (loading: boolean) => void; - setError: (error: Error | undefined) => void; - setAbortController: (abortController: AbortController | null) => void; - onResponse?: (response: Response) => void | Promise; - onFinish?: (prompt: string, completion: string) => void; - onError?: (error: Error) => void; - onData?: (data: JSONValue[]) => void; -}) { - try { - setLoading(true); - setError(undefined); - - const abortController = new AbortController(); - setAbortController(abortController); - - // Empty the completion immediately. - setCompletion(''); - - const res = await fetch(api, { - method: 'POST', - body: JSON.stringify({ - prompt, - ...body, - }), - credentials, - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - signal: abortController.signal, - }).catch(err => { - throw err; - }); - - if (onResponse) { - try { - await onResponse(res); - } catch (err) { - throw err; - } - } - - if (!res.ok) { - throw new Error( - (await res.text()) || 'Failed to fetch the chat response.', - ); - } - - if (!res.body) { - throw new Error('The response body is empty.'); - } - - let result = ''; - const reader = res.body.getReader(); - - const isComplexMode = res.headers.get(COMPLEX_HEADER) === 'true'; - - if (isComplexMode) { - for await (const { type, value } of readDataStream(reader, { - isAborted: () => abortController === null, - })) { - switch (type) { - case 'text': { - result += value; - setCompletion(result); - break; - } - case 'data': { - onData?.(value); - break; - } - } - } - } else { - const decoder = createChunkDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - // Update the completion state with the new message tokens. - result += decoder(value); - setCompletion(result); - - // The request has been aborted, stop reading the stream. - if (abortController === null) { - reader.cancel(); - break; - } - } - } - - if (onFinish) { - onFinish(prompt, result); - } - - setAbortController(null); - } catch (err) { - // Ignore abort errors as they are expected. - if ((err as any).name === 'AbortError') { - setAbortController(null); - } else { - if (err instanceof Error) { - if (onError) { - onError(err); - } - } - - setError(err as Error); - } - } finally { - setLoading(false); - } -} diff --git a/packages/copilot/src/lib/dashscope/index.ts b/packages/copilot/src/lib/dashscope/index.ts deleted file mode 100644 index 535ab2b0a..000000000 --- a/packages/copilot/src/lib/dashscope/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './call-chat-api' -export * from './call-completion-api' \ No newline at end of file diff --git a/packages/copilot/src/lib/engine.ts b/packages/copilot/src/lib/engine.ts index cb86cf01e..fd61db668 100644 --- a/packages/copilot/src/lib/engine.ts +++ b/packages/copilot/src/lib/engine.ts @@ -1,4 +1,6 @@ +import { tool } from '@langchain/core/tools' import { CompiledStateGraph } from '@langchain/langgraph/web' +import { z } from 'zod' import { CopilotCommand, CopilotContext } from './command' import { CopilotService } from './copilot' import { AIOptions, CopilotChatMessage } from './types' @@ -82,15 +84,15 @@ export interface CopilotEngine { /** * Chat with copilot by prompt - * - * @param prompt - * @param options + * + * @param prompt + * @param options */ chat(prompt: string, options?: CopilotChatOptions): Promise /** * Continue the conversation - * - * @param conversation + * + * @param conversation */ continue(conversation: CopilotChatConversation): Promise /** @@ -113,8 +115,8 @@ export interface CopilotEngine { commands?: () => CopilotCommand[] /** * Get command and it's context by command name - * - * @param name + * + * @param name */ getCommandWithContext(name: string): { command: CopilotCommand; context: CopilotContext } | null @@ -162,9 +164,26 @@ export interface CopilotEngine { /** * Execute command suggestion completion request - * - * @param input - * @param options + * + * @param input + * @param options */ - executeCommandSuggestion(input: string, options: {command: CopilotCommand; context: CopilotContext}): Promise + executeCommandSuggestion( + input: string, + options: { command: CopilotCommand; context: CopilotContext } + ): Promise } + +export const SuggestionOutputTool = tool((_) => '补全用户提示语', { + name: 'output_formatter', + description: 'Should always be used to properly format output', + schema: z.object({ + input: z.string().describe('Prompt after completion'), + suggestions: z.array(z.string().describe('One suggestion input')).describe('An array of suggestions') + }) +}) + +export type SuggestionOutput = string | { + input?: string; + suggestions?: Array +} \ No newline at end of file diff --git a/packages/copilot/src/lib/graph/react_agent_executor.ts b/packages/copilot/src/lib/graph/react_agent_executor.ts index 801e84ada..7c28fa98d 100644 --- a/packages/copilot/src/lib/graph/react_agent_executor.ts +++ b/packages/copilot/src/lib/graph/react_agent_executor.ts @@ -8,8 +8,9 @@ import { Runnable, RunnableInterface, RunnableLambda, + RunnableToolLike, } from "@langchain/core/runnables"; -import { DynamicTool, StructuredTool } from "@langchain/core/tools"; +import { DynamicTool, StructuredTool, StructuredToolInterface } from "@langchain/core/tools"; import { BaseLanguageModelCallOptions, @@ -70,7 +71,7 @@ export function createReactAgent( } = props; const schema: StateGraphArgs["channels"] = createCopilotAgentState() - let toolClasses: (StructuredTool | DynamicTool)[]; + let toolClasses: (StructuredToolInterface | DynamicTool | RunnableToolLike)[]; if (!Array.isArray(tools)) { toolClasses = tools.tools; } else { diff --git a/packages/copilot/src/lib/shared/call-chat-api.ts b/packages/copilot/src/lib/shared/call-chat-api.ts deleted file mode 100644 index 2b2f1f0ba..000000000 --- a/packages/copilot/src/lib/shared/call-chat-api.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { parseComplexResponse } from './parse-complex-response'; -import { - FunctionCall, - IdGenerator, - JSONValue, - Message, - OpenAIStream, - ToolCall, -} from 'ai'; -import { COMPLEX_HEADER, createChunkDecoder } from 'ai'; -import JSON5 from 'json5'; - -/** - * @deprecated use LangChain - */ -export async function callChatApi({ - api, - model, - messages, - body, - credentials, - headers, - abortController, - appendMessage, - restoreMessagesOnFailure, - onResponse, - onUpdate, - onFinish, - generateId, -}: { - api: string; - model: string; - messages: Omit[]; - body: Record; - credentials?: RequestCredentials; - headers?: HeadersInit; - abortController?: () => AbortController | null; - restoreMessagesOnFailure: () => void; - appendMessage: (message: Message) => void; - onResponse?: (response: Response) => void | Promise; - onUpdate: (merged: Message[], data: JSONValue[] | undefined) => void; - onFinish?: (message: Message) => void; - generateId: IdGenerator; -}): Promise { - body = { - messages, - stream: true, - ...body, - model, - } - const response = await fetch(api, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - signal: abortController?.()?.signal, - credentials, - }).catch(err => { - restoreMessagesOnFailure(); - throw err; - }); - - if (onResponse) { - try { - await onResponse(response); - } catch (err) { - throw err; - } - } - - if (!response.ok) { - restoreMessagesOnFailure(); - throw new Error( - (await response.text()) || 'Failed to fetch the chat response.', - ); - } - - if (!response.body) { - throw new Error('The response body is empty.'); - } - - // const reader = response.body.getReader(); - const reader = body['stream'] ? OpenAIStream(response).getReader() : response.body.getReader() - const isComplexMode = response.headers.get(COMPLEX_HEADER) === 'true'; - - if (isComplexMode) { - return await parseComplexResponse({ - reader, - abortControllerRef: - abortController != null ? { current: abortController() } : undefined, - update: onUpdate, - onFinish(prefixMap) { - if (onFinish && prefixMap.text != null) { - onFinish(prefixMap.text); - } - }, - generateId, - }); - } else { - const createdAt = new Date(); - const decode = createChunkDecoder(false); - - // TODO-STREAMDATA: Remove this once Stream Data is not experimental - let streamedResponse = ''; - const replyId = generateId(); - const responseMessage: Message = { - id: replyId, - createdAt, - content: '', - role: 'assistant', - }; - - // TODO-STREAMDATA: Remove this once Stream Data is not experimental - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - // Update the chat state with the new message tokens. - streamedResponse += decode(value); - - if (streamedResponse.startsWith('{"function_call":')) { - // While the function call is streaming, it will be a string. - responseMessage['function_call'] = streamedResponse; - } else if (streamedResponse.startsWith('{"tool_calls":')) { - // While the tool calls are streaming, it will be a string. - responseMessage['tool_calls'] = streamedResponse; - } else if(body['stream']) { - responseMessage['content'] = streamedResponse; - } - - appendMessage({ ...responseMessage }); - - // The request has been aborted, stop reading the stream. - if (abortController?.() === null) { - reader.cancel(); - break; - } - } - - if (body['stream']) { - if (streamedResponse.startsWith('{"function_call":')) { - // Once the stream is complete, the function call is parsed into an object. - const parsedFunctionCall: FunctionCall = - JSON.parse(streamedResponse).function_call; - - responseMessage['function_call'] = parsedFunctionCall; - - appendMessage({ ...responseMessage }); - } - if (streamedResponse.startsWith('{"tool_calls":')) { - // Once the stream is complete, the tool calls are parsed into an array. - const parsedToolCalls: ToolCall[] = - JSON.parse(streamedResponse).tool_calls; - - responseMessage['tool_calls'] = parsedToolCalls; - - appendMessage({ ...responseMessage }); - } - } else { - const parsedResponse = JSON5.parse(streamedResponse) - const message = parsedResponse.choices[0]?.message - if (message) { - if (message.function_call) { - const parsedFunctionCall: FunctionCall = message.function_call; - responseMessage['function_call'] = parsedFunctionCall; - // appendMessage({ ...responseMessage }); - } else if (message.tool_calls) { - const parsedToolCalls: ToolCall[] = message.tool_calls; - responseMessage['tool_calls'] = parsedToolCalls; - - } else { - responseMessage['content'] = message.content - } - // appendMessage({ ...responseMessage }); - } - } - - if (onFinish) { - onFinish(responseMessage); - } - - return responseMessage; - } -} diff --git a/packages/copilot/src/lib/shared/call-completion-api.ts b/packages/copilot/src/lib/shared/call-completion-api.ts deleted file mode 100644 index 8fed924d2..000000000 --- a/packages/copilot/src/lib/shared/call-completion-api.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { readDataStream } from './read-data-stream'; -import { JSONValue } from 'ai'; -import { COMPLEX_HEADER, createChunkDecoder } from 'ai'; - -/** - * @deprecated use LangChain - */ -export async function callCompletionApi({ - api, - prompt, - credentials, - headers, - body, - setCompletion, - setLoading, - setError, - setAbortController, - onResponse, - onFinish, - onError, - onData, -}: { - api: string; - prompt: string; - credentials?: RequestCredentials; - headers?: HeadersInit; - body: Record; - setCompletion: (completion: string) => void; - setLoading: (loading: boolean) => void; - setError: (error: Error | undefined) => void; - setAbortController: (abortController: AbortController | null) => void; - onResponse?: (response: Response) => void | Promise; - onFinish?: (prompt: string, completion: string) => void; - onError?: (error: Error) => void; - onData?: (data: JSONValue[]) => void; -}) { - try { - setLoading(true); - setError(undefined); - - const abortController = new AbortController(); - setAbortController(abortController); - - // Empty the completion immediately. - setCompletion(''); - - const res = await fetch(api, { - method: 'POST', - body: JSON.stringify({ - prompt, - ...body, - }), - credentials, - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - signal: abortController.signal, - }).catch(err => { - throw err; - }); - - if (onResponse) { - try { - await onResponse(res); - } catch (err) { - throw err; - } - } - - if (!res.ok) { - throw new Error( - (await res.text()) || 'Failed to fetch the chat response.', - ); - } - - if (!res.body) { - throw new Error('The response body is empty.'); - } - - let result = ''; - const reader = res.body.getReader(); - - const isComplexMode = res.headers.get(COMPLEX_HEADER) === 'true'; - - if (isComplexMode) { - for await (const { type, value } of readDataStream(reader, { - isAborted: () => abortController === null, - })) { - switch (type) { - case 'text': { - result += value; - setCompletion(result); - break; - } - case 'data': { - onData?.(value); - break; - } - } - } - } else { - const decoder = createChunkDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - // Update the completion state with the new message tokens. - result += decoder(value); - setCompletion(result); - - // The request has been aborted, stop reading the stream. - if (abortController === null) { - reader.cancel(); - break; - } - } - } - - if (onFinish) { - onFinish(prompt, result); - } - - setAbortController(null); - } catch (err) { - // Ignore abort errors as they are expected. - if ((err as any).name === 'AbortError') { - setAbortController(null); - } else { - if (err instanceof Error) { - if (onError) { - onError(err); - } - } - - setError(err as Error); - } - } finally { - setLoading(false); - } -} diff --git a/packages/copilot/src/lib/shared/functions.ts b/packages/copilot/src/lib/shared/functions.ts deleted file mode 100644 index 94d471fa3..000000000 --- a/packages/copilot/src/lib/shared/functions.ts +++ /dev/null @@ -1,107 +0,0 @@ -// import { ChatRequest, FunctionCall, Message, nanoid } from 'ai' -// import { ChatCompletionCreateParams } from 'openai/resources' -// import JSON5 from 'json5' - -// export const defaultCopilotContextCategories = ['global'] - -// /** -// * @deprecated use LangChain -// */ -// export type FunctionCallHandler = ( -// chatMessages: Message[], -// functionCall: FunctionCall, -// conversationId: string -// ) => Promise - -// export type FunctionCallHandlerOptions = { -// conversationId: string -// messages: Message[] -// } - -// export function entryPointsToFunctionCallHandler(entryPoints: AnnotatedFunction[]): FunctionCallHandler { -// return async (chatMessages, functionCall, conversationId): Promise => { -// const entrypointsByFunctionName: Record> = {} -// for (const entryPoint of entryPoints) { -// entrypointsByFunctionName[entryPoint.name] = entryPoint -// } - -// const entryPointFunction = entrypointsByFunctionName[functionCall.name || ''] -// if (entryPointFunction) { -// let parsedFunctionCallArguments: Record[] = [] -// if (functionCall.arguments) { -// parsedFunctionCallArguments = JSON5.parse(functionCall.arguments) -// } - -// const paramsInCorrectOrder: any[] = [] -// for (const arg of entryPointFunction.argumentAnnotations) { -// paramsInCorrectOrder.push(parsedFunctionCallArguments[arg.name as keyof typeof parsedFunctionCallArguments]) -// } - -// // return await entryPointFunction.implementation(...paramsInCorrectOrder) -// const result = await entryPointFunction.implementation(...paramsInCorrectOrder, {conversationId, messages: chatMessages}) -// if (!result) { -// return -// } -// if (typeof result === 'string') { -// return result -// } -// const functionResponse: ChatRequest = { -// messages: [ -// ...chatMessages, -// { -// ...result, -// id: nanoid(), -// name: functionCall.name, -// role: 'function' as const -// } -// ] -// } -// return functionResponse -// } -// } -// } - -// /** -// * @deprecated use LangChain -// */ -// export function entryPointsToChatCompletionFunctions( -// entryPoints: AnnotatedFunction[] -// ): ChatCompletionCreateParams.Function[] { -// return entryPoints.map(annotatedFunctionToChatCompletionFunction) -// } - -// /** -// * @deprecated use LangChain -// */ -// export function annotatedFunctionToChatCompletionFunction( -// annotatedFunction: AnnotatedFunction -// ): ChatCompletionCreateParams.Function { -// // Create the parameters object based on the argumentAnnotations -// const parameters: { [key: string]: any } = {} -// for (const arg of annotatedFunction.argumentAnnotations) { -// // isolate the args we should forward inline -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// const { name, required, ...forwardedArgs } = arg -// parameters[arg.name] = forwardedArgs -// } - -// const requiredParameterNames: string[] = [] -// for (const arg of annotatedFunction.argumentAnnotations) { -// if (arg.required) { -// requiredParameterNames.push(arg.name) -// } -// } - -// // Create the ChatCompletionFunctions object -// const chatCompletionFunction: ChatCompletionCreateParams.Function = { -// name: annotatedFunction.name, -// description: annotatedFunction.description, -// parameters: { -// type: 'object', -// properties: parameters, -// required: requiredParameterNames -// } -// } - -// return chatCompletionFunction -// } diff --git a/packages/copilot/src/lib/shared/parse-complex-response.ts b/packages/copilot/src/lib/shared/parse-complex-response.ts deleted file mode 100644 index 0b08690dd..000000000 --- a/packages/copilot/src/lib/shared/parse-complex-response.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { readDataStream } from './read-data-stream'; -import type { ChatRequest, FunctionCall, JSONValue, Message, ToolCall } from 'ai'; -import { nanoid } from 'ai'; - -type PrefixMap = { - text?: Message; - function_call?: Message & { - role: 'assistant'; - function_call: FunctionCall; - }; - tool_calls?: Message & { - role: 'assistant'; - tool_calls: ToolCall[]; - }; - data: JSONValue[]; -}; - -/** - * @deprecated use LangChain - */ -export async function parseComplexResponse({ - reader, - abortControllerRef, - update, - onFinish, - generateId = nanoid, - getCurrentDate = () => new Date(), -}: { - reader: ReadableStreamDefaultReader; - abortControllerRef?: { - current: AbortController | null; - }; - update: (merged: Message[], data: JSONValue[] | undefined) => void; - onFinish?: (prefixMap: PrefixMap) => void; - generateId?: () => string; - getCurrentDate?: () => Date; -}) { - const createdAt = getCurrentDate(); - const prefixMap: PrefixMap = { - data: [], - }; - - // we create a map of each prefix, and for each prefixed message we push to the map - for await (const { type, value } of readDataStream(reader, { - isAborted: () => abortControllerRef?.current === null, - })) { - if (type === 'text') { - if (prefixMap['text']) { - prefixMap['text'] = { - ...prefixMap['text'], - content: (prefixMap['text'].content || '') + value, - }; - } else { - prefixMap['text'] = { - id: generateId(), - role: 'assistant', - content: value, - createdAt, - }; - } - } - - let functionCallMessage: Message | null = null; - - if (type === 'function_call') { - prefixMap['function_call'] = { - id: generateId(), - role: 'assistant', - content: '', - function_call: value.function_call, - name: value.function_call.name, - createdAt, - }; - - functionCallMessage = prefixMap['function_call']; - } - - let toolCallMessage: Message | null = null; - - if (type === 'tool_calls') { - prefixMap['tool_calls'] = { - id: generateId(), - role: 'assistant', - content: '', - tool_calls: value.tool_calls, - createdAt, - }; - - toolCallMessage = prefixMap['tool_calls']; - } - - if (type === 'data') { - prefixMap['data'].push(...value); - } - - const responseMessage = prefixMap['text']; - - // We add function & tool calls and response messages to the messages[], but data is its own thing - const merged = [ - functionCallMessage, - toolCallMessage, - responseMessage, - ].filter(Boolean) as Message[]; - - update(merged, [...prefixMap['data']]); // make a copy of the data array - } - - onFinish?.(prefixMap); - - return { - messages: [ - prefixMap.text, - prefixMap.function_call, - prefixMap.tool_calls, - ].filter(Boolean) as Message[], - data: prefixMap.data - } -} diff --git a/packages/copilot/src/lib/shared/process-chat-stream.ts b/packages/copilot/src/lib/shared/process-chat-stream.ts deleted file mode 100644 index 6071deb0c..000000000 --- a/packages/copilot/src/lib/shared/process-chat-stream.ts +++ /dev/null @@ -1,255 +0,0 @@ -// /* eslint-disable no-constant-condition */ -// /* eslint-disable no-inner-declarations */ -// import { -// ChatRequest, -// JSONValue, -// Message, -// ToolCall, -// } from 'ai'; -// import { CopilotChatMessage } from '../types'; - -// /** -// * @deprecated use LangChain -// */ -// export async function processChatStream({ -// getStreamedResponse, -// experimental_onFunctionCall, -// experimental_onToolCall, -// updateChatRequest, -// getCurrentMessages, -// conversationId -// }: { -// getStreamedResponse: () => Promise< -// Message | { messages: Message[]; data: JSONValue[] } -// >; -// experimental_onFunctionCall?: FunctionCallHandler; -// experimental_onToolCall?: ( -// chatMessages: Message[], -// toolCalls: ToolCall[], -// ) => Promise; -// updateChatRequest: (chatRequest: ChatRequest) => void; -// getCurrentMessages: () => Message[]; -// conversationId: string; -// }): Promise { - -// let retry = 0 -// while (true) { -// // TODO-STREAMDATA: This should be { const { messages: streamedResponseMessages, data } = -// // await getStreamedResponse(} once Stream Data is not experimental -// const messagesAndDataOrJustMessage = await getStreamedResponse(); - -// // Using experimental stream data -// if ('messages' in messagesAndDataOrJustMessage) { -// let hasFollowingResponse = false; - -// for (const message of messagesAndDataOrJustMessage.messages) { -// // See if the message has a complete function call or tool call -// if ( -// (message.function_call === undefined || -// typeof message.function_call === 'string') && -// (message.tool_calls === undefined || -// typeof message.tool_calls === 'string') -// ) { -// continue; -// } - -// hasFollowingResponse = true; -// // Try to handle function call -// if (experimental_onFunctionCall) { -// const functionCall = message.function_call; -// // Make sure functionCall is an object -// // If not, we got tool calls instead of function calls -// if (typeof functionCall !== 'object') { -// console.warn( -// 'experimental_onFunctionCall should not be defined when using tools', -// ); -// continue; -// } - -// // User handles the function call in their own functionCallHandler. -// // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. -// // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. - -// const functionCallResponse: ChatRequest | string | void = -// await experimental_onFunctionCall( -// getCurrentMessages(), -// functionCall, -// conversationId -// ); - -// // If the user does not return anything as a result of the function call, the loop will break. -// if (functionCallResponse === undefined) { -// hasFollowingResponse = false; -// break; -// } -// if (typeof functionCallResponse === 'string') { -// updateChatRequest({ -// messages: [ -// { -// ...message, -// content: functionCallResponse -// } -// ] -// }) -// break -// } - -// if (functionCallResponse) -// // A function call response was returned. -// // The updated chat with function call response will be sent to the API in the next iteration of the loop. -// updateChatRequest(functionCallResponse); -// } -// // Try to handle tool call -// if (experimental_onToolCall) { -// const toolCalls = message.tool_calls; -// // Make sure toolCalls is an array of objects -// // If not, we got function calls instead of tool calls -// if ( -// !Array.isArray(toolCalls) || -// toolCalls.some(toolCall => typeof toolCall !== 'object') -// ) { -// console.warn( -// 'experimental_onToolCall should not be defined when using tools', -// ); -// continue; -// } - -// // User handles the function call in their own functionCallHandler. -// // The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler. -// // If the "arguments" JSON is malformed due to model error the user will have to handle that themselves. -// const toolCallResponse: ChatRequest | string | void = -// await experimental_onToolCall(getCurrentMessages(), toolCalls); - -// // If the user does not return anything as a result of the function call, the loop will break. -// if (toolCallResponse === undefined) { -// hasFollowingResponse = false; -// break; -// } -// if (typeof toolCallResponse === 'string') { -// hasFollowingResponse = false; -// break -// } - -// if (toolCallResponse) -// // A function call response was returned. -// // The updated chat with function call response will be sent to the API in the next iteration of the loop. -// updateChatRequest(toolCallResponse); -// } -// } -// if (!hasFollowingResponse) { -// break; -// } -// } else { -// const streamedResponseMessage = messagesAndDataOrJustMessage; - -// // TODO-STREAMDATA: Remove this once Stream Data is not experimental -// if ( -// (streamedResponseMessage.function_call === undefined || -// typeof streamedResponseMessage.function_call === 'string') && -// (streamedResponseMessage.tool_calls === undefined || -// typeof streamedResponseMessage.tool_calls === 'string') -// ) { -// return messagesAndDataOrJustMessage -// } - -// // If we get here and are expecting a function call, the message should have one, if not warn and continue -// if (experimental_onFunctionCall) { -// const functionCall = streamedResponseMessage.function_call; -// if (!(typeof functionCall === 'object')) { -// console.warn( -// 'experimental_onFunctionCall should not be defined when using tools', -// ); -// continue; -// } -// const functionCallResponse: ChatRequest | string | void = -// await experimental_onFunctionCall(getCurrentMessages(), functionCall, conversationId); - -// // If the user does not return anything as a result of the function call, the loop will break. -// if (functionCallResponse === undefined) break; -// if (typeof functionCallResponse === 'string') { -// // Success Info! -// return { -// ...streamedResponseMessage, -// content: functionCallResponse // `✅ Function '${functionCall.name}' call successful!` -// } -// } -// if (retry > 3) { -// break -// } -// retry += 1 -// // Type check -// if (functionCallResponse) { -// // A function call response was returned. -// // The updated chat with function call response will be sent to the API in the next iteration of the loop. -// fixFunctionCallArguments(functionCallResponse); -// updateChatRequest(functionCallResponse); -// } -// } -// // If we get here and are expecting a tool call, the message should have one, if not warn and continue -// if (experimental_onToolCall) { -// const toolCalls = streamedResponseMessage.tool_calls; -// if (!(typeof toolCalls === 'object')) { -// console.warn( -// 'experimental_onToolCall should not be defined when using functions', -// ); -// continue; -// } -// const toolCallResponse: ChatRequest | string | void = -// await experimental_onToolCall(getCurrentMessages(), toolCalls); - -// // If the user does not return anything as a result of the function call, the loop will break. -// if (toolCallResponse === undefined) break; -// if (typeof toolCallResponse === 'string') { -// updateChatRequest({ -// messages: [ -// { -// ...streamedResponseMessage, -// content: toolCallResponse -// } -// ] -// }) -// break -// } -// // Type check -// if (toolCallResponse) { -// // A function call response was returned. -// // The updated chat with function call response will be sent to the API in the next iteration of the loop. -// fixFunctionCallArguments(toolCallResponse); -// updateChatRequest(toolCallResponse); -// } -// } - -// // Make sure function call arguments are sent back to the API as a string -// function fixFunctionCallArguments(response: ChatRequest) { -// for (const message of response.messages) { -// if (message.tool_calls !== undefined) { -// for (const toolCall of message.tool_calls) { -// if (typeof toolCall === 'object') { -// if ( -// toolCall.function.arguments && -// typeof toolCall.function.arguments !== 'string' -// ) { -// toolCall.function.arguments = JSON.stringify( -// toolCall.function.arguments, -// ); -// } -// } -// } -// } -// if (message.function_call !== undefined) { -// if (typeof message.function_call === 'object') { -// if ( -// message.function_call.arguments && -// typeof message.function_call.arguments !== 'string' -// ) { -// message.function_call.arguments = JSON.stringify( -// message.function_call.arguments, -// ); -// } -// } -// } -// } -// } -// } -// } -// } diff --git a/packages/copilot/src/lib/shared/read-data-stream.ts b/packages/copilot/src/lib/shared/read-data-stream.ts deleted file mode 100644 index 33db13294..000000000 --- a/packages/copilot/src/lib/shared/read-data-stream.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { StreamPartType, parseStreamPart } from './stream-parts'; - -const NEWLINE = '\n'.charCodeAt(0); - -// concatenates all the chunks into a single Uint8Array -function concatChunks(chunks: Uint8Array[], totalLength: number) { - const concatenatedChunks = new Uint8Array(totalLength); - - let offset = 0; - for (const chunk of chunks) { - concatenatedChunks.set(chunk, offset); - offset += chunk.length; - } - chunks.length = 0; - - return concatenatedChunks; -} - -/** - * @deprecated use LangChain - */ -export async function* readDataStream( - reader: ReadableStreamDefaultReader, - { - isAborted, - }: { - isAborted?: () => boolean; - } = {}, -): AsyncGenerator { - // implementation note: this slightly more complex algorithm is required - // to pass the tests in the edge environment. - - const decoder = new TextDecoder(); - const chunks: Uint8Array[] = []; - let totalLength = 0; - - while (true) { - const { value } = await reader.read(); - - if (value) { - chunks.push(value); - totalLength += value.length; - if (value[value.length - 1] !== NEWLINE) { - // if the last character is not a newline, we have not read the whole JSON value - continue; - } - } - - if (chunks.length === 0) { - break; // we have reached the end of the stream - } - - const concatenatedChunks = concatChunks(chunks, totalLength); - totalLength = 0; - - const streamParts = decoder - .decode(concatenatedChunks, { stream: true }) - .split('\n') - .filter(line => line !== '') // splitting leaves an empty string at the end - .map(parseStreamPart); - - for (const streamPart of streamParts) { - yield streamPart; - } - - // The request has been aborted, stop reading the stream. - if (isAborted?.()) { - reader.cancel(); - break; - } - } -} diff --git a/packages/copilot/src/lib/shared/stream-parts.ts b/packages/copilot/src/lib/shared/stream-parts.ts deleted file mode 100644 index fac30283b..000000000 --- a/packages/copilot/src/lib/shared/stream-parts.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { - AssistantMessage, - DataMessage, - FunctionCall, - JSONValue, - ToolCall, -} from 'ai'; -import { StreamString } from 'ai'; - -export interface StreamPart { - code: CODE; - name: NAME; - parse: (value: JSONValue) => { type: NAME; value: TYPE }; -} - -const textStreamPart: StreamPart<'0', 'text', string> = { - code: '0', - name: 'text', - parse: (value: JSONValue) => { - if (typeof value !== 'string') { - throw new Error('"text" parts expect a string value.'); - } - return { type: 'text', value }; - }, -}; - -const functionCallStreamPart: StreamPart< - '1', - 'function_call', - { function_call: FunctionCall } -> = { - code: '1', - name: 'function_call', - parse: (value: JSONValue) => { - if ( - value == null || - typeof value !== 'object' || - !('function_call' in value) || - typeof value['function_call'] !== 'object' || - value['function_call'] == null || - !('name' in value['function_call']) || - !('arguments' in value['function_call']) || - typeof value['function_call']['name'] !== 'string' || - typeof value['function_call']['arguments'] !== 'string' - ) { - throw new Error( - '"function_call" parts expect an object with a "function_call" property.', - ); - } - - return { - type: 'function_call', - value: value as unknown as { function_call: FunctionCall }, - }; - }, -}; - -const dataStreamPart: StreamPart<'2', 'data', Array> = { - code: '2', - name: 'data', - parse: (value: JSONValue) => { - if (!Array.isArray(value)) { - throw new Error('"data" parts expect an array value.'); - } - - return { type: 'data', value }; - }, -}; - -const errorStreamPart: StreamPart<'3', 'error', string> = { - code: '3', - name: 'error', - parse: (value: JSONValue) => { - if (typeof value !== 'string') { - throw new Error('"error" parts expect a string value.'); - } - return { type: 'error', value }; - }, -}; - -const assistantMessageStreamPart: StreamPart< - '4', - 'assistant_message', - AssistantMessage -> = { - code: '4', - name: 'assistant_message', - parse: (value: JSONValue) => { - if ( - value == null || - typeof value !== 'object' || - !('id' in value) || - !('role' in value) || - !('content' in value) || - typeof value['id'] !== 'string' || - typeof value['role'] !== 'string' || - value['role'] !== 'assistant' || - !Array.isArray(value['content']) || - !value['content'].every( - item => - item != null && - typeof item === 'object' && - 'type' in item && - item['type'] === 'text' && - 'text' in item && - item['text'] != null && - typeof item['text'] === 'object' && - 'value' in item['text'] && - typeof item['text']['value'] === 'string', - ) - ) { - throw new Error( - '"assistant_message" parts expect an object with an "id", "role", and "content" property.', - ); - } - - return { - type: 'assistant_message', - value: value as AssistantMessage, - }; - }, -}; - -const assistantControlDataStreamPart: StreamPart< - '5', - 'assistant_control_data', - { - threadId: string; - messageId: string; - } -> = { - code: '5', - name: 'assistant_control_data', - parse: (value: JSONValue) => { - if ( - value == null || - typeof value !== 'object' || - !('threadId' in value) || - !('messageId' in value) || - typeof value['threadId'] !== 'string' || - typeof value['messageId'] !== 'string' - ) { - throw new Error( - '"assistant_control_data" parts expect an object with a "threadId" and "messageId" property.', - ); - } - - return { - type: 'assistant_control_data', - value: { - threadId: value['threadId'], - messageId: value['messageId'], - }, - }; - }, -}; - -const dataMessageStreamPart: StreamPart<'6', 'data_message', DataMessage> = { - code: '6', - name: 'data_message', - parse: (value: JSONValue) => { - if ( - value == null || - typeof value !== 'object' || - !('role' in value) || - !('data' in value) || - typeof value['role'] !== 'string' || - value['role'] !== 'data' - ) { - throw new Error( - '"data_message" parts expect an object with a "role" and "data" property.', - ); - } - - return { - type: 'data_message', - value: value as DataMessage, - }; - }, -}; - -const toolCallStreamPart: StreamPart< - '7', - 'tool_calls', - { tool_calls: ToolCall[] } -> = { - code: '7', - name: 'tool_calls', - parse: (value: JSONValue) => { - if ( - value == null || - typeof value !== 'object' || - !('tool_calls' in value) || - typeof value['tool_calls'] !== 'object' || - value['tool_calls'] == null || - !Array.isArray(value['tool_calls']) || - value['tool_calls'].some(tc => { - tc == null || - typeof tc !== 'object' || - !('id' in tc) || - typeof tc['id'] !== 'string' || - !('type' in tc) || - typeof tc['type'] !== 'string' || - !('function' in tc) || - tc['function'] == null || - typeof tc['function'] !== 'object' || - !('arguments' in tc['function']) || - typeof tc['function']['name'] !== 'string' || - typeof tc['function']['arguments'] !== 'string'; - }) - ) { - throw new Error( - '"tool_calls" parts expect an object with a ToolCallPayload.', - ); - } - - return { - type: 'tool_calls', - value: value as unknown as { tool_calls: ToolCall[] }, - }; - }, -}; - -const streamParts = [ - textStreamPart, - functionCallStreamPart, - dataStreamPart, - errorStreamPart, - assistantMessageStreamPart, - assistantControlDataStreamPart, - dataMessageStreamPart, - toolCallStreamPart, -] as const; - -// union type of all stream parts -type StreamParts = - | typeof textStreamPart - | typeof functionCallStreamPart - | typeof dataStreamPart - | typeof errorStreamPart - | typeof assistantMessageStreamPart - | typeof assistantControlDataStreamPart - | typeof dataMessageStreamPart - | typeof toolCallStreamPart; - -/** - * Maps the type of a stream part to its value type. - */ -type StreamPartValueType = { - [P in StreamParts as P['name']]: ReturnType['value']; -}; - -export type StreamPartType = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export const streamPartsByCode = { - [textStreamPart.code]: textStreamPart, - [functionCallStreamPart.code]: functionCallStreamPart, - [dataStreamPart.code]: dataStreamPart, - [errorStreamPart.code]: errorStreamPart, - [assistantMessageStreamPart.code]: assistantMessageStreamPart, - [assistantControlDataStreamPart.code]: assistantControlDataStreamPart, - [dataMessageStreamPart.code]: dataMessageStreamPart, - [toolCallStreamPart.code]: toolCallStreamPart, -} as const; - -/** - * The map of prefixes for data in the stream - * - * - 0: Text from the LLM response - * - 1: (OpenAI) function_call responses - * - 2: custom JSON added by the user using `Data` - * - 6: (OpenAI) tool_call responses - * - * Example: - * ``` - * 0:Vercel - * 0:'s - * 0: AI - * 0: AI - * 0: SDK - * 0: is great - * 0:! - * 2: { "someJson": "value" } - * 1: {"function_call": {"name": "get_current_weather", "arguments": "{\\n\\"location\\": \\"Charlottesville, Virginia\\",\\n\\"format\\": \\"celsius\\"\\n}"}} - * 6: {"tool_call": {"id": "tool_0", "type": "function", "function": {"name": "get_current_weather", "arguments": "{\\n\\"location\\": \\"Charlottesville, Virginia\\",\\n\\"format\\": \\"celsius\\"\\n}"}}} - *``` - */ -export const StreamStringPrefixes = { - [textStreamPart.name]: textStreamPart.code, - [functionCallStreamPart.name]: functionCallStreamPart.code, - [dataStreamPart.name]: dataStreamPart.code, - [errorStreamPart.name]: errorStreamPart.code, - [assistantMessageStreamPart.name]: assistantMessageStreamPart.code, - [assistantControlDataStreamPart.name]: assistantControlDataStreamPart.code, - [dataMessageStreamPart.name]: dataMessageStreamPart.code, - [toolCallStreamPart.name]: toolCallStreamPart.code, -} as const; - -export const validCodes = streamParts.map(part => part.code); - -/** - * Parses a stream part from a string. - * - * @param line The string to parse. - * @returns The parsed stream part. - * @throws An error if the string cannot be parsed. - */ -export const parseStreamPart = (line: string): StreamPartType => { - const firstSeparatorIndex = line.indexOf(':'); - - if (firstSeparatorIndex === -1) { - throw new Error('Failed to parse stream string. No separator found.'); - } - - const prefix = line.slice(0, firstSeparatorIndex); - - if (!validCodes.includes(prefix as keyof typeof streamPartsByCode)) { - throw new Error(`Failed to parse stream string. Invalid code ${prefix}.`); - } - - const code = prefix as keyof typeof streamPartsByCode; - - const textValue = line.slice(firstSeparatorIndex + 1); - const jsonValue: JSONValue = JSON.parse(textValue); - - return streamPartsByCode[code].parse(jsonValue); -}; - -/** - * Prepends a string with a prefix from the `StreamChunkPrefixes`, JSON-ifies it, - * and appends a new line. - * - * It ensures type-safety for the part type and value. - */ -export function formatStreamPart( - type: T, - value: StreamPartValueType[T], -): StreamString { - const streamPart = streamParts.find(part => part.name === type); - - if (!streamPart) { - throw new Error(`Invalid stream part type: ${type}`); - } - - return `${streamPart.code}:${JSON.stringify(value)}\n`; -} diff --git a/packages/copilot/src/lib/streams/dashscope-stream.ts b/packages/copilot/src/lib/streams/dashscope-stream.ts deleted file mode 100644 index bd6b9cf53..000000000 --- a/packages/copilot/src/lib/streams/dashscope-stream.ts +++ /dev/null @@ -1,643 +0,0 @@ -import { CreateMessage, - FunctionCall, - JSONValue, - ToolCall, - createStreamDataTransformer, - createChunkDecoder, - AIStream, - trimStartOfStreamHelper, - type AIStreamCallbacksAndOptions, - FunctionCallPayload, - readableFromAsyncIterable, - createCallbacksTransformer, - ToolCallPayload, -} from 'ai'; -import { formatStreamPart } from '../shared/stream-parts'; - -export type OpenAIStreamCallbacks = AIStreamCallbacksAndOptions & { - /** - * @example - * ```js - * const response = await openai.chat.completions.create({ - * model: 'gpt-3.5-turbo-0613', - * stream: true, - * messages, - * functions, - * }) - * - * const stream = OpenAIStream(response, { - * experimental_onFunctionCall: async (functionCallPayload, createFunctionCallMessages) => { - * // ... run your custom logic here - * const result = await myFunction(functionCallPayload) - * - * // Ask for another completion, or return a string to send to the client as an assistant message. - * return await openai.chat.completions.create({ - * model: 'gpt-3.5-turbo-0613', - * stream: true, - * // Append the relevant "assistant" and "function" call messages - * messages: [...messages, ...createFunctionCallMessages(result)], - * functions, - * }) - * } - * }) - * ``` - */ - experimental_onFunctionCall?: ( - functionCallPayload: FunctionCallPayload, - createFunctionCallMessages: ( - functionCallResult: JSONValue, - ) => CreateMessage[], - ) => Promise< - Response | undefined | void | string | AsyncIterableOpenAIStreamReturnTypes - >; - /** - * @example - * ```js - * const response = await openai.chat.completions.create({ - * model: 'gpt-3.5-turbo-1106', // or gpt-4-1106-preview - * stream: true, - * messages, - * tools, - * tool_choice: "auto", // auto is default, but we'll be explicit - * }) - * - * const stream = OpenAIStream(response, { - * experimental_onToolCall: async (toolCallPayload, appendToolCallMessages) => { - * let messages: CreateMessage[] = [] - * // There might be multiple tool calls, so we need to iterate through them - * for (const tool of toolCallPayload.tools) { - * // ... run your custom logic here - * const result = await myFunction(tool.function) - * // Append the relevant "assistant" and "tool" call messages - * appendToolCallMessage({tool_call_id:tool.id, function_name:tool.function.name, tool_call_result:result}) - * } - * // Ask for another completion, or return a string to send to the client as an assistant message. - * return await openai.chat.completions.create({ - * model: 'gpt-3.5-turbo-1106', // or gpt-4-1106-preview - * stream: true, - * // Append the results messages, calling appendToolCallMessage without - * // any arguments will jsut return the accumulated messages - * messages: [...messages, ...appendToolCallMessage()], - * tools, - * tool_choice: "auto", // auto is default, but we'll be explicit - * }) - * } - * }) - * ``` - */ - experimental_onToolCall?: ( - toolCallPayload: ToolCallPayload, - appendToolCallMessage: (result?: { - tool_call_id: string; - function_name: string; - tool_call_result: JSONValue; - }) => CreateMessage[], - ) => Promise< - Response | undefined | void | string | AsyncIterableOpenAIStreamReturnTypes - >; -}; - -// https://help.aliyun.com/zh/dashscope/developer-reference/api-details -interface ChatCompletionChunk { - request_id: string; - output: ChatCompletionChunkChoice; - usage: CompletionUsage; -} - -interface ChatCompletionChunkChoice { - delta: string; - finish_reason: - | 'stop' - | 'length' - | 'tool_calls' - | 'content_filter' - | 'function_call' - | null; -} - -// https://github.com/openai/openai-node/blob/07b3504e1c40fd929f4aae1651b83afc19e3baf8/src/resources/chat/completions.ts#L123-L139 -// Updated to https://github.com/openai/openai-node/commit/84b43280089eacdf18f171723591856811beddce -interface ChoiceDelta { - /** - * The contents of the chunk message. - */ - content?: string | null; - - /** - * The name and arguments of a function that should be called, as generated by the - * model. - */ - function_call?: FunctionCall; - - /** - * The role of the author of this message. - */ - role?: 'system' | 'user' | 'assistant' | 'tool'; - - tool_calls?: Array; -} - -// From https://github.com/openai/openai-node/blob/master/src/resources/chat/completions.ts -// Updated to https://github.com/openai/openai-node/commit/84b43280089eacdf18f171723591856811beddce -interface DeltaToolCall { - index: number; - - /** - * The ID of the tool call. - */ - id?: string; - - /** - * The function that the model called. - */ - function?: ToolCallFunction; - - /** - * The type of the tool. Currently, only `function` is supported. - */ - type?: 'function'; -} - -// From https://github.com/openai/openai-node/blob/master/src/resources/chat/completions.ts -// Updated to https://github.com/openai/openai-node/commit/84b43280089eacdf18f171723591856811beddce -interface ToolCallFunction { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments?: string; - - /** - * The name of the function to call. - */ - name?: string; -} - -/** - * https://github.com/openai/openai-node/blob/3ec43ee790a2eb6a0ccdd5f25faa23251b0f9b8e/src/resources/completions.ts#L28C1-L64C1 - * Completions API. Streamed and non-streamed responses are the same. - */ -interface Completion { - /** - * A unique identifier for the completion. - */ - request_id: string; - /** - * https://help.aliyun.com/zh/dashscope/response-status-codes - */ - code: string; - message: string; - output: CompletionChoice; - usage: CompletionUsage; -} - -interface CompletionChoice { - /** - * The reason the model stopped generating tokens. This will be `stop` if the model - * hit a natural stop point or a provided stop sequence, or `length` if the maximum - * number of tokens specified in the request was reached. - */ - finish_reason: 'stop' | 'length' | 'content_filter'; - - // edited: Removed CompletionChoice.logProbs and replaced with any - logprobs: any | null; - - text: string; -} - -export interface CompletionUsage { - /** - * Usage statistics for the completion request. - */ - - /** - * Number of tokens in the generated completion. - */ - completion_tokens: number; - - /** - * Number of tokens in the prompt. - */ - prompt_tokens: number; - - /** - * Total number of tokens used in the request (prompt + completion). - */ - total_tokens: number; -} - -/** - * Creates a parser function for processing the OpenAI stream data. - * The parser extracts and trims text content from the JSON data. This parser - * can handle data for chat or completion models. - * - * @return {(data: string) => string | void} A parser function that takes a JSON string as input and returns the extracted text content or nothing. - */ -function parseOpenAIStream(): (data: string) => string | void { - const extract = chunkToText(); - return data => extract(JSON.parse(data) as OpenAIStreamReturnTypes); -} - -/** - * Reads chunks from OpenAI's new Streamable interface, which is essentially - * the same as the old Response body interface with an included SSE parser - * doing the parsing for us. - */ -async function* streamable(stream: AsyncIterableOpenAIStreamReturnTypes) { - const extract = chunkToText(); - for await (const chunk of stream) { - const text = extract(chunk); - if (text) yield text; - } -} - -function chunkToText(): (chunk: OpenAIStreamReturnTypes) => string | void { - const trimStartOfStream = trimStartOfStreamHelper(); - let isFunctionStreamingIn: boolean; - return json => { - if (isError(json)) { - throw new Error(json.message) - } - if (isChatCompletionChunk(json)) { - const delta = json.output.delta - // if (delta.function_call?.name) { - // isFunctionStreamingIn = true; - // return `{"function_call": {"name": "${delta.function_call.name}", "arguments": "`; - // } else if (delta.tool_calls?.[0]?.function?.name) { - // isFunctionStreamingIn = true; - // const toolCall = delta.tool_calls[0]; - // if (toolCall.index === 0) { - // return `{"tool_calls":[ {"id": "${toolCall.id}", "type": "function", "function": {"name": "${toolCall.function?.name}", "arguments": "`; - // } else { - // return `"}}, {"id": "${toolCall.id}", "type": "function", "function": {"name": "${toolCall.function?.name}", "arguments": "`; - // } - // } else if (delta.function_call?.arguments) { - // return cleanupArguments(delta.function_call?.arguments); - // } else if (delta.tool_calls?.[0].function?.arguments) { - // return cleanupArguments(delta.tool_calls?.[0]?.function?.arguments); - // } else - if ( - isFunctionStreamingIn && - (json.output?.finish_reason === 'function_call' || - json.output?.finish_reason === 'stop') - ) { - isFunctionStreamingIn = false; // Reset the flag - return '"}}'; - } else if ( - isFunctionStreamingIn && - json.output?.finish_reason === 'tool_calls' - ) { - isFunctionStreamingIn = false; // Reset the flag - return '"}}]}'; - } - } - - const text = trimStartOfStream( - isChatCompletionChunk(json) && json.output.delta - ? json.output.delta - : isCompletion(json) - ? json.output.text - : '', - ); - return text; - }; - - function cleanupArguments(argumentChunk: string) { - const escapedPartialJson = argumentChunk - .replace(/\\/g, '\\\\') // Replace backslashes first to prevent double escaping - .replace(/\//g, '\\/') // Escape slashes - .replace(/"/g, '\\"') // Escape double quotes - .replace(/\n/g, '\\n') // Escape new lines - .replace(/\r/g, '\\r') // Escape carriage returns - .replace(/\t/g, '\\t') // Escape tabs - .replace(/\f/g, '\\f'); // Escape form feeds - - return `${escapedPartialJson}`; - } -} - -const __internal__OpenAIFnMessagesSymbol = Symbol( - 'internal_openai_fn_messages', -); - -type AsyncIterableOpenAIStreamReturnTypes = - | AsyncIterable - | AsyncIterable; - -type ExtractType = T extends AsyncIterable ? U : never; - -type OpenAIStreamReturnTypes = - ExtractType; - -function isChatCompletionChunk( - data: OpenAIStreamReturnTypes, -): data is ChatCompletionChunk { - return ( - 'output' in data && - data.output && - 'text' in data.output - ); -} - -function isCompletion(data: OpenAIStreamReturnTypes): data is Completion { - return ( - 'output' in data && - data.output && - 'text' in data.output - ); -} - -function isError(data: OpenAIStreamReturnTypes): data is Completion { - return ( - 'code' in data && - !!data.code - ) -} - -export function DashScopeStream( - res: Response | AsyncIterableOpenAIStreamReturnTypes, - callbacks?: OpenAIStreamCallbacks, -): ReadableStream { - // Annotate the internal `messages` property for recursive function calls - const cb: - | undefined - | (OpenAIStreamCallbacks & { - [__internal__OpenAIFnMessagesSymbol]?: CreateMessage[]; - }) = callbacks; - - let stream: ReadableStream; - if (Symbol.asyncIterator in res) { - stream = readableFromAsyncIterable(streamable(res)).pipeThrough( - createCallbacksTransformer( - cb?.experimental_onFunctionCall || cb?.experimental_onToolCall - ? { - ...cb, - onFinal: undefined, - } - : { - ...cb, - }, - ), - ); - } else { - stream = AIStream( - res, - parseOpenAIStream(), - cb?.experimental_onFunctionCall || cb?.experimental_onToolCall - ? { - ...cb, - onFinal: undefined, - } - : { - ...cb, - }, - ); - } - - if (cb && (cb.experimental_onFunctionCall || cb.experimental_onToolCall)) { - const functionCallTransformer = createFunctionCallTransformer(cb); - return stream.pipeThrough(functionCallTransformer); - } else { - return stream.pipeThrough( - createStreamDataTransformer(cb?.experimental_streamData), - ); - } -} - -function createFunctionCallTransformer( - callbacks: OpenAIStreamCallbacks & { - [__internal__OpenAIFnMessagesSymbol]?: CreateMessage[]; - }, -): TransformStream { - const textEncoder = new TextEncoder(); - let isFirstChunk = true; - let aggregatedResponse = ''; - let aggregatedFinalCompletionResponse = ''; - let isFunctionStreamingIn = false; - - const functionCallMessages: CreateMessage[] = - callbacks[__internal__OpenAIFnMessagesSymbol] || []; - - const isComplexMode = callbacks?.experimental_streamData; - const decode = createChunkDecoder(); - - return new TransformStream({ - async transform(chunk, controller): Promise { - const message = decode(chunk); - aggregatedFinalCompletionResponse += message; - - const shouldHandleAsFunction = - isFirstChunk && - (message.startsWith('{"function_call":') || - message.startsWith('{"tool_calls":')); - - if (shouldHandleAsFunction) { - isFunctionStreamingIn = true; - aggregatedResponse += message; - isFirstChunk = false; - return; - } - - // Stream as normal - if (!isFunctionStreamingIn) { - controller.enqueue( - isComplexMode - ? textEncoder.encode(formatStreamPart('text', message)) - : chunk, - ); - return; - } else { - aggregatedResponse += message; - } - }, - async flush(controller): Promise { - try { - if ( - !isFirstChunk && - isFunctionStreamingIn && - (callbacks.experimental_onFunctionCall || - callbacks.experimental_onToolCall) - ) { - isFunctionStreamingIn = false; - const payload = JSON.parse(aggregatedResponse); - // Append the function call message to the list - let newFunctionCallMessages: CreateMessage[] = [ - ...functionCallMessages, - ]; - - let functionResponse: - | Response - | undefined - | void - | string - | AsyncIterableOpenAIStreamReturnTypes - | undefined = undefined; - // This callbacks.experimental_onFunctionCall check should not be necessary but TS complains - if (callbacks.experimental_onFunctionCall) { - // If the user is using the experimental_onFunctionCall callback, they should not be using tools - // if payload.function_call is not defined by time we get here we must have gotten a tool response - // and the user had defined experimental_onToolCall - if (payload.function_call === undefined) { - console.warn( - 'experimental_onFunctionCall should not be defined when using tools', - ); - } - - const argumentsPayload = JSON.parse( - payload.function_call.arguments, - ); - - functionResponse = await callbacks.experimental_onFunctionCall( - { - name: payload.function_call.name, - arguments: argumentsPayload, - }, - result => { - // Append the function call request and result messages to the list - newFunctionCallMessages = [ - ...functionCallMessages, - { - role: 'assistant', - content: '', - function_call: payload.function_call, - }, - { - role: 'function', - name: payload.function_call.name, - content: JSON.stringify(result), - }, - ]; - // Return it to the user - return newFunctionCallMessages; - }, - ); - } - if (callbacks.experimental_onToolCall) { - const toolCalls: ToolCallPayload = { - tools: [], - }; - for (const tool of payload.tool_calls) { - toolCalls.tools.push({ - id: tool.id, - type: 'function', - func: { - name: tool.function.name, - arguments: tool.function.arguments, - }, - }); - } - let responseIndex = 0; - try { - functionResponse = await callbacks.experimental_onToolCall( - toolCalls, - result => { - if (result) { - const { tool_call_id, function_name, tool_call_result } = - result; - // Append the function call request and result messages to the list - newFunctionCallMessages = [ - ...newFunctionCallMessages, - // Only append the assistant message if it's the first response - ...(responseIndex === 0 - ? [ - { - role: 'assistant' as const, - content: '', - tool_calls: payload.tool_calls.map( - (tc: ToolCall) => ({ - id: tc.id, - type: 'function', - function: { - name: tc.function.name, - // we send the arguments an object to the user, but as the API expects a string, we need to stringify it - arguments: JSON.stringify( - tc.function.arguments, - ), - }, - }), - ), - }, - ] - : []), - // Append the function call result message - { - role: 'tool', - tool_call_id, - name: function_name, - content: JSON.stringify(tool_call_result), - }, - ]; - responseIndex++; - } - // Return it to the user - return newFunctionCallMessages; - }, - ); - } catch (e) { - console.error('Error calling experimental_onToolCall:', e); - } - } - - if (!functionResponse) { - // The user didn't do anything with the function call on the server and wants - // to either do nothing or run it on the client - // so we just return the function call as a message - controller.enqueue( - textEncoder.encode( - isComplexMode - ? formatStreamPart( - payload.function_call ? 'function_call' : 'tool_calls', - // parse to prevent double-encoding: - JSON.parse(aggregatedResponse), - ) - : aggregatedResponse, - ), - ); - return; - } else if (typeof functionResponse === 'string') { - // The user returned a string, so we just return it as a message - controller.enqueue( - isComplexMode - ? textEncoder.encode(formatStreamPart('text', functionResponse)) - : textEncoder.encode(functionResponse), - ); - return; - } - - // Recursively: - - // We don't want to trigger onStart or onComplete recursively - // so we remove them from the callbacks - // see https://github.com/vercel/ai/issues/351 - const filteredCallbacks: OpenAIStreamCallbacks = { - ...callbacks, - onStart: undefined, - }; - // We only want onFinal to be called the _last_ time - callbacks.onFinal = undefined; - - const openAIStream = DashScopeStream(functionResponse, { - ...filteredCallbacks, - [__internal__OpenAIFnMessagesSymbol]: newFunctionCallMessages, - } as AIStreamCallbacksAndOptions); - - const reader = openAIStream.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - controller.enqueue(value); - } - } - } finally { - if (callbacks.onFinal && aggregatedFinalCompletionResponse) { - await callbacks.onFinal(aggregatedFinalCompletionResponse); - } - } - }, - }); -} diff --git a/packages/copilot/src/lib/streams/index.ts b/packages/copilot/src/lib/streams/index.ts deleted file mode 100644 index fb5a51bc0..000000000 --- a/packages/copilot/src/lib/streams/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dashscope-stream' \ No newline at end of file diff --git a/packages/copilot/src/lib/types/providers.ts b/packages/copilot/src/lib/types/providers.ts index 33b29a7c5..1735248f7 100644 --- a/packages/copilot/src/lib/types/providers.ts +++ b/packages/copilot/src/lib/types/providers.ts @@ -168,7 +168,7 @@ export const AI_PROVIDERS: Record> = { [AiProvider.Ollama]: { models: [ { - id: 'llama2', + id: 'llama3', name: 'LLama 3' }, { @@ -182,7 +182,11 @@ export const AI_PROVIDERS: Record> = { { id: 'phi3', name: 'Phi-3' - } + }, + { + id: 'llama3-groq-tool-use', + name: 'Llama3 Groq Tool Use' + }, ] } } From 762093bc22b300ad966dfbbdb78b0bdaf2513767 Mon Sep 17 00:00:00 2001 From: meta-d Date: Thu, 25 Jul 2024 17:59:40 +0800 Subject: [PATCH 25/53] feat: suggestion examples --- apps/cloud/src/app/@core/copilot/few-shot.ts | 6 +- .../cloud/src/app/@core/copilot/references.ts | 2 +- .../model/copilot/modeler/command.ts | 27 ++---- .../model/copilot/modeler/graph.ts | 8 +- .../model/copilot/modeler/types.ts | 1 + .../model/copilot/table/command.ts | 4 +- .../src/model-member/member.service.ts | 4 +- packages/contracts/src/ai.model.ts | 3 +- .../src/lib/chat/chat.component.html | 14 ++- .../src/lib/chat/chat.component.ts | 34 ++++++- .../src/lib/services/engine.service.ts | 13 ++- packages/copilot/src/lib/command.ts | 17 ++-- packages/copilot/src/lib/utils.ts | 17 ++++ .../copilot-example.service.ts | 93 ++++++++++++------- 14 files changed, 165 insertions(+), 78 deletions(-) diff --git a/apps/cloud/src/app/@core/copilot/few-shot.ts b/apps/cloud/src/app/@core/copilot/few-shot.ts index bafa09ba3..8a888c611 100644 --- a/apps/cloud/src/app/@core/copilot/few-shot.ts +++ b/apps/cloud/src/app/@core/copilot/few-shot.ts @@ -1,7 +1,11 @@ import { inject, Provider } from '@angular/core' import { SemanticSimilarityExampleSelector } from '@langchain/core/example_selectors' import { FewShotPromptTemplate, PromptTemplate } from '@langchain/core/prompts' -import { ExampleVectorStoreRetrieverInput, NgmCommandFewShotPromptToken, NgmCopilotService } from '@metad/copilot-angular' +import { + ExampleVectorStoreRetrieverInput, + NgmCommandFewShotPromptToken, + NgmCopilotService +} from '@metad/copilot-angular' import { CopilotExampleService } from '../services/copilot-example.service' import { ExampleVectorStoreRetriever } from './example-vector-retriever' diff --git a/apps/cloud/src/app/@core/copilot/references.ts b/apps/cloud/src/app/@core/copilot/references.ts index 5179d7f1a..366282acd 100644 --- a/apps/cloud/src/app/@core/copilot/references.ts +++ b/apps/cloud/src/app/@core/copilot/references.ts @@ -3,7 +3,7 @@ import { ExampleVectorStoreRetrieverInput, NgmCopilotService } from '@metad/copi import { CopilotExampleService } from '../services/copilot-example.service' import { ExampleVectorStoreRetriever } from './example-vector-retriever' -export function injectExampleReferencesRetriever(command: string, fields?: ExampleVectorStoreRetrieverInput) { +export function injectExampleRetriever(command: string, fields?: ExampleVectorStoreRetrieverInput) { const copilotService = inject(NgmCopilotService) const copilotExampleService = inject(CopilotExampleService) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts index 014805544..d1d35b5a0 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/command.ts @@ -1,25 +1,15 @@ import { inject } from '@angular/core' +import { ChatPromptTemplate } from '@langchain/core/prompts' import { CopilotAgentType, CreateGraphOptions, Team } from '@metad/copilot' import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' -import { injectAgentFewShotTemplate } from 'apps/cloud/src/app/@core/copilot' +import { injectAgentFewShotTemplate, injectExampleRetriever } from 'apps/cloud/src/app/@core/copilot' import { NGXLogger } from 'ngx-logger' import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME } from '../cube' import { DIMENSION_MODELER_NAME } from '../dimension' import { injectCreateModelerGraph } from './graph' -import { - ChatPromptTemplate, - PromptTemplate, - SystemMessagePromptTemplate, - AIMessagePromptTemplate, - HumanMessagePromptTemplate, -} from "@langchain/core/prompts"; -import { - AIMessage, - HumanMessage, - SystemMessage, -} from "@langchain/core/messages"; +import { MODELER_COMMAND_NAME } from './types' export function injectModelerCommand() { const logger = inject(NGXLogger) @@ -27,9 +17,9 @@ export function injectModelerCommand() { const modelService = inject(SemanticModelService) const createModelerGraph = injectCreateModelerGraph() - const commandName = 'modeler' - const fewShotPrompt = injectAgentFewShotTemplate(commandName, { k: 1, vectorStore: null }) - return injectCopilotCommand(commandName, { + const examplesRetriever = injectExampleRetriever(MODELER_COMMAND_NAME, { k: 5, vectorStore: null }) + const fewShotPrompt = injectAgentFewShotTemplate(MODELER_COMMAND_NAME, { k: 1, vectorStore: null }) + return injectCopilotCommand(MODELER_COMMAND_NAME, { alias: 'm', description: translate.instant('PAC.MODEL.Copilot.CommandModelerDesc', { Default: 'Describe model requirements or structure' @@ -51,10 +41,11 @@ export function injectModelerCommand() { llm }) }, + examplesRetriever, suggestion: { promptTemplate: ChatPromptTemplate.fromMessages([ - ["system", `用简短的一句话补全用户可能的提问,直接输出答案不要解释`], - ["human", '{input}'], + ['system', `用简短的一句话补全用户可能的提问,仅输出源提示后面补全的内容,不需要解释,使用与原提示同样的语言`], + ['human', '{input}'] ]) } }) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts index c0032e0aa..ea4c10e92 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/graph.ts @@ -1,18 +1,18 @@ import { inject } from '@angular/core' import { RunnableLambda } from '@langchain/core/runnables' import { ToolNode } from '@langchain/langgraph/prebuilt' -import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web' +import { START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web' import { CreateGraphOptions, Team } from '@metad/copilot' +import { injectExampleRetriever } from 'apps/cloud/src/app/@core/copilot' import { SemanticModelService } from '../../model.service' import { CUBE_MODELER_NAME, injectRunCubeModeler } from '../cube' import { DIMENSION_MODELER_NAME, injectRunDimensionModeler } from '../dimension/' import { injectQueryTablesTool, injectSelectTablesTool } from '../tools' import { createSupervisorAgent } from './supervisor' import { ModelerState } from './types' -import { injectExampleReferencesRetriever } from 'apps/cloud/src/app/@core/copilot' const superState: StateGraphArgs['channels'] = { - ...Team.createState(), + ...Team.createState() } export function injectCreateModelerGraph() { @@ -22,7 +22,7 @@ export function injectCreateModelerGraph() { const selectTablesTool = injectSelectTablesTool() const queryTablesTool = injectQueryTablesTool() - const referencesRetriever = injectExampleReferencesRetriever('modeler/references', {k: 3, vectorStore: null}) + const referencesRetriever = injectExampleRetriever('modeler/references', { k: 3, vectorStore: null }) const dimensions = modelService.dimensions diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts index 3504eea5b..1f3ab1d42 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/modeler/types.ts @@ -1,5 +1,6 @@ import { Team } from '@metad/copilot' +export const MODELER_COMMAND_NAME = 'modeler' export const PLANNER_NAME = 'Planner' export const SUPERVISOR_NAME = 'Supervisor' diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts index 65d2f523d..071dad59a 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/table/command.ts @@ -3,7 +3,7 @@ import { CopilotAgentType } from '@metad/copilot' import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' -import { injectAgentFewShotTemplate } from '../../../../../@core/copilot' +import { injectAgentFewShotTemplate, injectExampleRetriever } from '../../../../../@core/copilot' import { injectTableCreator } from './graph' import { TABLE_COMMAND_NAME } from './types' @@ -12,6 +12,7 @@ export function injectTableCommand() { const translate = inject(TranslateService) const createTableCreator = injectTableCreator() + const examplesRetriever = injectExampleRetriever(TABLE_COMMAND_NAME, { k: 5, score: 0.8, vectorStore: null }) const fewShotPrompt = injectAgentFewShotTemplate(TABLE_COMMAND_NAME, { k: 1, vectorStore: null }) return injectCopilotCommand(TABLE_COMMAND_NAME, { alias: 't', @@ -24,6 +25,7 @@ export function injectTableCommand() { interruptBefore: ['tools'] }, fewShotPrompt, + examplesRetriever, createGraph: createTableCreator }) } diff --git a/packages/analytics/src/model-member/member.service.ts b/packages/analytics/src/model-member/member.service.ts index db7d79b51..3fd111647 100644 --- a/packages/analytics/src/model-member/member.service.ts +++ b/packages/analytics/src/model-member/member.service.ts @@ -2,7 +2,7 @@ import { PGVectorStore, PGVectorStoreArgs } from '@langchain/community/vectorsto import { Document } from '@langchain/core/documents' import type { EmbeddingsInterface } from '@langchain/core/embeddings' import { OpenAIEmbeddings } from '@langchain/openai' -import { AIEmbeddings, ISemanticModel, ISemanticModelEntity } from '@metad/contracts' +import { OpenAIEmbeddingsProviders, ISemanticModel, ISemanticModelEntity } from '@metad/contracts' import { EntityType, PropertyDimension, @@ -156,7 +156,7 @@ export class SemanticModelMemberService extends TenantOrganizationAwareCrudServi } const result = await this.copilotService.findAll({ where }) const copilot = result.items[0] - if (copilot && copilot.enabled && AIEmbeddings.includes(copilot.provider)) { + if (copilot && copilot.enabled && OpenAIEmbeddingsProviders.includes(copilot.provider)) { const id = modelId ? `${modelId}${cube ? ':' + cube : ''}` : 'default' if (!this.vectorStores.has(id)) { const embeddings = new OpenAIEmbeddings({ diff --git a/packages/contracts/src/ai.model.ts b/packages/contracts/src/ai.model.ts index 3c5d0723d..fee44a80e 100644 --- a/packages/contracts/src/ai.model.ts +++ b/packages/contracts/src/ai.model.ts @@ -16,4 +16,5 @@ export enum AiBusinessRole { SupplyChainExpert = 'supply_chain_expert', } -export const AIEmbeddings = [AiProvider.OpenAI, AiProvider.Azure] \ No newline at end of file +export const OpenAIEmbeddingsProviders = [AiProvider.OpenAI, AiProvider.Azure] +export const OllamaEmbeddingsProviders = [AiProvider.Ollama] \ No newline at end of file diff --git a/packages/copilot-angular/src/lib/chat/chat.component.html b/packages/copilot-angular/src/lib/chat/chat.component.html index 8f5247dde..265c1b026 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.html +++ b/packages/copilot-angular/src/lib/chat/chat.component.html @@ -189,15 +189,15 @@
@if (command() || context()) { -
+
@if (command()) { {{command().name}} @@ -213,6 +213,14 @@ {{command().description}} }
+ +
+ @for (example of examples(); track example.text) { + {{example.text}} + } +
} diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index d7846176a..ba18f980e 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -50,6 +50,7 @@ import { CopilotContextItem, SuggestionOutput, nanoid, + nonBlank, } from '@metad/copilot' import { TranslateModule } from '@ngx-translate/core' import { @@ -65,6 +66,7 @@ import { debounceTime, delay, filter, + map, of, startWith, switchMap, @@ -277,7 +279,7 @@ export class NgmCopilotChatComponent { public promptControl = new FormControl('') readonly prompt = toSignal(this.promptControl.valueChanges.pipe(filter((value) => typeof value === 'string')), { initialValue: '' }) - readonly #promptWords = computed(() => this.prompt()?.split(' ')) + readonly #promptWords = computed(() => this.prompt()?.split(' ') ?? []) readonly lastWord = computed(() => this.#promptWords()[this.#promptWords().length - 1]) readonly #contextWord = computed(() => this.#promptWords() @@ -298,6 +300,14 @@ export class NgmCopilotChatComponent { return words.splice(0, words.length - 1).join(' ') }) + /** + * The first word is a command + */ + readonly commandWord = computed(() => { + const firstWord = this.#promptWords().filter(nonBlank)[0] + return firstWord?.startsWith('/') ? firstWord : null + }) + readonly commandWithContext = computed(() => { const prompt = this.prompt() if (prompt && prompt.startsWith('/')) { @@ -423,6 +433,24 @@ export class NgmCopilotChatComponent { readonly messageCopied = signal([]) readonly editingMessageId = signal(null) + readonly input = toSignal(this.promptControl.valueChanges + .pipe( + debounceTime(AUTO_SUGGESTION_DEBOUNCE_TIME), + filter((text) => !AUTO_SUGGESTION_STOP.includes(text.slice(-1))), + map((text) => text.trim()) + )) + readonly examplesRetriever = computed(() => this.command()?.examplesRetriever) + readonly examples = derivedAsync(() => { + const examplesRetriever = this.examplesRetriever() + const input = this.input() + if (!examplesRetriever) { + return null + } + return examplesRetriever.invoke(input).then((docs) => docs.map((doc) => ({ + text: doc.metadata['input'] + }))) + }) + /** |-------------------------------------------------------------------------- | Copilot @@ -807,4 +835,8 @@ export class NgmCopilotChatComponent { await this.copilotEngine.finish(conversation) } + pickExample(text: string) { + this.promptControl.setValue(`${this.commandWord()} ${text}`, {emitEvent: false}) + this.promptCompletion.set(null) + } } diff --git a/packages/copilot-angular/src/lib/services/engine.service.ts b/packages/copilot-angular/src/lib/services/engine.service.ts index 3ef79f654..fe04fd2cd 100644 --- a/packages/copilot-angular/src/lib/services/engine.service.ts +++ b/packages/copilot-angular/src/lib/services/engine.service.ts @@ -1062,10 +1062,7 @@ export class NgmCopilotEngineService implements CopilotEngine { input: string, options: { command: CopilotCommand; context: CopilotContext; signal?: AbortSignal } ): Promise { - const { command, context, signal } = options - // Context content - const contextContent = context ? await recognizeContext(input, context) : null - const params = await recognizeContextParams(input, context) + const { command, signal } = options if (command.fewShotPrompt) { input = await command.fewShotPrompt.format({ input }) @@ -1079,7 +1076,13 @@ export class NgmCopilotEngineService implements CopilotEngine { if (command.suggestion?.promptTemplate) { const chain = command.suggestion.promptTemplate.pipe((secondaryLLM ?? llm).bindTools([SuggestionOutputTool])) .pipe(new StringOutputParser()) - return await chain.invoke({ input, context: contextContent, signal, verbose }) + return await chain.invoke({ + input, + role: this.copilot.rolePrompt(), + language: this.copilot.languagePrompt(), + signal, + verbose + }) } else { throw new Error('No completion template found') } diff --git a/packages/copilot/src/lib/command.ts b/packages/copilot/src/lib/command.ts index 55c496850..53cf62670 100644 --- a/packages/copilot/src/lib/command.ts +++ b/packages/copilot/src/lib/command.ts @@ -1,5 +1,6 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models' import { BaseStringPromptTemplate, ChatPromptTemplate } from '@langchain/core/prompts' +import { BaseRetriever } from '@langchain/core/retrievers' import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools' import { BaseCheckpointSaver, CompiledStateGraph, StateGraph } from '@langchain/langgraph/web' import { ChatOpenAI } from '@langchain/openai' @@ -26,12 +27,8 @@ export interface CopilotCommand { * Description of the command */ description: string - // /** - // * @deprecated use suggestions - // * - // * Examples of the command usage - // */ - // examples?: string[] + + examplesRetriever?: BaseRetriever /** * Input suggestions */ @@ -72,8 +69,12 @@ export interface CopilotCommand { interruptAfter?: string[] } - createGraph?: (options: CreateGraphOptions) => Promise, "__start__" | "tools" | "agent" | string> | - CompiledStateGraph, "__start__" | "tools" | "agent" | string>> + createGraph?: ( + options: CreateGraphOptions + ) => Promise< + | StateGraph, '__start__' | 'tools' | 'agent' | string> + | CompiledStateGraph, '__start__' | 'tools' | 'agent' | string> + > // For history management historyCursor?: () => number diff --git a/packages/copilot/src/lib/utils.ts b/packages/copilot/src/lib/utils.ts index 6da5b3864..8fe89d72c 100644 --- a/packages/copilot/src/lib/utils.ts +++ b/packages/copilot/src/lib/utils.ts @@ -14,6 +14,23 @@ export function nonNullable(value: T): value is NonNullable { return value != null } +export function isNil(value: unknown): value is null | undefined { + return value == null +} + +export function isString(value: unknown): value is string { + return typeof value ==='string' || value instanceof String +} + +export function isBlank(value: unknown) { + return isNil(value) || isString(value) && !value.trim() +} + +export function nonBlank(value: T): value is NonNullable { + return !isBlank(value) +} + + /** * Split the prompt into command and prompt * diff --git a/packages/server/src/copilot-example/copilot-example.service.ts b/packages/server/src/copilot-example/copilot-example.service.ts index 9a1bc810b..d078724b4 100644 --- a/packages/server/src/copilot-example/copilot-example.service.ts +++ b/packages/server/src/copilot-example/copilot-example.service.ts @@ -1,9 +1,19 @@ +import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama' import { PGVectorStore, PGVectorStoreArgs } from '@langchain/community/vectorstores/pgvector' import { Document } from '@langchain/core/documents' import type { EmbeddingsInterface } from '@langchain/core/embeddings' import { MaxMarginalRelevanceSearchOptions } from '@langchain/core/vectorstores' import { OpenAIEmbeddings } from '@langchain/openai' -import { AIEmbeddings, AiBusinessRole, ICopilotExample, ICopilotRole } from '@metad/contracts' +import { + AiBusinessRole, + AiProvider, + AiProviderRole, + ICopilot, + ICopilotExample, + ICopilotRole, + OllamaEmbeddingsProviders, + OpenAIEmbeddingsProviders +} from '@metad/contracts' import { Inject, Injectable, Logger } from '@nestjs/common' import { CommandBus } from '@nestjs/cqrs' import { InjectRepository } from '@nestjs/typeorm' @@ -81,7 +91,7 @@ export class CopilotExampleService extends TenantAwareCrudService (1 - _score) > (score ?? 0.7)).map(([doc]) => doc) + return results.filter(([, _score]) => 1 - _score > (score ?? 0.7)).map(([doc]) => doc) } return [] @@ -116,7 +126,7 @@ export class CopilotExampleService extends TenantAwareCrudService example.role)) - for await (const role of roles) { - try { - await this.commandBus.execute(new CopilotRoleCreateCommand(role)) - } catch (error) {} + if (roles) { + for await (const role of roles) { + try { + await this.commandBus.execute(new CopilotRoleCreateCommand(role)) + } catch (error) {} + } } // Auto create role if not existed @@ -261,12 +287,12 @@ export class CopilotExampleService extends TenantAwareCrudService role ? item.role === role : !item.role) + const examples = entities.filter((item) => (role ? item.role === role : !item.role)).map((example) => ({...example, input: example.input?.trim(), output: example.output?.trim() })) const roleExamples = await Promise.all(examples.map((entity) => super.create(entity))) results.push(...roleExamples) if (roleExamples.length && vectorStore) { await vectorStore.addExamples(roleExamples) - await Promise.all(roleExamples.map((entity) => super.update(entity.id, { vector: true }))) + await Promise.all(roleExamples.map((entity) => super.update(entity.id, { provider: vectorStore.provider, vector: true }))) } } @@ -278,6 +304,7 @@ class PGMemberVectorStore { vectorStore: PGVectorStore constructor( + public provider: AiProvider, public embeddings: EmbeddingsInterface, _dbConfig: PGVectorStoreArgs ) { From 690c7b3858707d1abeee00cf5d92e71a72dddce7 Mon Sep 17 00:00:00 2001 From: meta-d Date: Thu, 25 Jul 2024 18:38:27 +0800 Subject: [PATCH 26/53] feat: hierarchy command agent --- .../copilot/dimension/hierarchy.command.ts | 23 +++++-------------- .../model/copilot/dimension/tools.ts | 10 +++++++- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/hierarchy.command.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/hierarchy.command.ts index ce95eb2a9..633cad1b5 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/hierarchy.command.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/hierarchy.command.ts @@ -1,39 +1,28 @@ import { Signal, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { ActivatedRoute, ActivatedRouteSnapshot, Router } from '@angular/router' -import { DynamicStructuredTool } from '@langchain/core/tools' +import { Router } from '@angular/router' import { CopilotAgentType } from '@metad/copilot' import { createAgentPromptTemplate, injectCopilotCommand } from '@metad/copilot-angular' import { makeTablePrompt } from '@metad/core' -import { EntityType, PropertyHierarchy } from '@metad/ocap-core' +import { EntityType } from '@metad/ocap-core' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' import { firstValueFrom } from 'rxjs' import { ModelDimensionService } from '../../dimension/dimension.service' import { SemanticModelService } from '../../model.service' import { markdownTableData } from '../../utils' -import { HierarchySchema } from '../schema' +import { injectCreateHierarchyTool } from './tools' import { timeLevelFormatter } from './types' export function injectHierarchyCommand(dimensionService: ModelDimensionService, tableTypes: Signal) { const logger = inject(NGXLogger) const translate = inject(TranslateService) const modelService = inject(SemanticModelService) - const route = inject(ActivatedRoute) const router = inject(Router) - const dimension = toSignal(dimensionService.dimension$) + const createHierarchyTool = injectCreateHierarchyTool() - const createHierarchyTool = new DynamicStructuredTool({ - name: 'createHierarchy', - description: 'Create or edit hierarchy in dimension.', - schema: HierarchySchema, - func: async (h) => { - logger.debug(`Execute copilot action 'createHierarchy':`, h) - dimensionService.upsertHierarchy(h as Partial) - return translate.instant('PAC.MODEL.Copilot.CreatedHierarchy', { Default: 'Created hierarchy!' }) - } - }) + const dimension = toSignal(dimensionService.dimension$) let urlSnapshot = router.url @@ -75,7 +64,7 @@ ${tableTypes() '\n{context}' + `\n{system}` ).partial({ - system: systemContext, + system: systemContext }) return { diff --git a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/tools.ts b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/tools.ts index 41d3477db..288720257 100644 --- a/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/tools.ts +++ b/apps/cloud/src/app/features/semantic-model/model/copilot/dimension/tools.ts @@ -1,5 +1,6 @@ import { inject } from '@angular/core' import { DynamicStructuredTool } from '@langchain/core/tools' +import { nanoid } from '@metad/copilot' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' import { z } from 'zod' @@ -40,7 +41,14 @@ export function injectCreateHierarchyTool() { }), func: async ({ dimension, hierarchy }) => { logger.debug(`Execute copilot action 'createHierarchy' for dimension: '${dimension}' using:`, hierarchy) - modelService.upsertHierarchy({ dimension, hierarchy }) + modelService.upsertHierarchy({ + dimension, + hierarchy: { + ...hierarchy, + __id__: nanoid(), + levels: hierarchy.levels?.map((level) => ({ ...level, __id__: nanoid() })) + } + }) return translate.instant('PAC.MODEL.Copilot.CreatedHierarchy', { Default: 'Created hierarchy!' }) } }) From b976d72cd7660f0dce640b4b646a25f5a2881cad Mon Sep 17 00:00:00 2001 From: meta-d Date: Thu, 25 Jul 2024 22:02:50 +0800 Subject: [PATCH 27/53] feat: page command agent --- .../features/story/story/story.component.ts | 2 +- libs/story-angular/i18n/zhHans.ts | 1 + libs/story-angular/story/copilot/index.ts | 2 +- .../story/copilot/page.command.ts | 99 +++++++------------ .../story/copilot/page/command.ts | 27 +++++ .../story-angular/story/copilot/page/graph.ts | 43 ++++++++ .../story-angular/story/copilot/page/index.ts | 1 + .../story-angular/story/copilot/page/tools.ts | 48 +++++++++ .../story-angular/story/copilot/page/types.ts | 13 +++ .../story/copilot/schema/page.schema.ts | 3 + 10 files changed, 174 insertions(+), 65 deletions(-) create mode 100644 libs/story-angular/story/copilot/page/command.ts create mode 100644 libs/story-angular/story/copilot/page/graph.ts create mode 100644 libs/story-angular/story/copilot/page/index.ts create mode 100644 libs/story-angular/story/copilot/page/tools.ts create mode 100644 libs/story-angular/story/copilot/page/types.ts diff --git a/apps/cloud/src/app/features/story/story/story.component.ts b/apps/cloud/src/app/features/story/story/story.component.ts index ceefd0e7b..703f3ce31 100644 --- a/apps/cloud/src/app/features/story/story/story.component.ts +++ b/apps/cloud/src/app/features/story/story/story.component.ts @@ -173,7 +173,7 @@ export class StoryDesignerComponent extends TranslationBaseComponent implements |-------------------------------------------------------------------------- */ #styleCommand = injectStoryStyleCommand(this.storyService) - #pageCommand = injectStoryPageCommand(this.storyService) + #pageCommand = injectStoryPageCommand() #widgetCommand = injectStoryWidgetCommand() // #widgetStyleCommand = injectWidgetStyleCommand(this.storyService) #storyCommand = injectStoryCommand() diff --git a/libs/story-angular/i18n/zhHans.ts b/libs/story-angular/i18n/zhHans.ts index c49a384a0..f3a7bf007 100644 --- a/libs/story-angular/i18n/zhHans.ts +++ b/libs/story-angular/i18n/zhHans.ts @@ -862,6 +862,7 @@ export const ZhHans = { StoryStyleCommandDesc: '请描述想要的故事样式', StoryPageCommandDesc: '请描述新的故事页面', PleaseSelectWidget: '请先选择一个微件', + CommandPageDesc: '描述你想要的页面', PredefinedPrompts: [ '/story 设置为暗色主题', '/story-style 设置科技感渐变色背景颜色', diff --git a/libs/story-angular/story/copilot/index.ts b/libs/story-angular/story/copilot/index.ts index 836043ebd..831fc7f40 100644 --- a/libs/story-angular/story/copilot/index.ts +++ b/libs/story-angular/story/copilot/index.ts @@ -1,6 +1,6 @@ export * from './measure.command' -export * from './page.command' export * from './schema/' export * from './style.command' export * from './types' export * from './widget/index' +export * from './page/index' \ No newline at end of file diff --git a/libs/story-angular/story/copilot/page.command.ts b/libs/story-angular/story/copilot/page.command.ts index 342ccbbd1..a9d69c654 100644 --- a/libs/story-angular/story/copilot/page.command.ts +++ b/libs/story-angular/story/copilot/page.command.ts @@ -1,13 +1,9 @@ import { inject } from '@angular/core' -import { calcEntityTypePrompt, zodToProperties } from '@metad/core' +import { calcEntityTypePrompt } from '@metad/core' import { injectCopilotCommand } from '@metad/copilot-angular' import { EntityType } from '@metad/ocap-core' -import { NxStoryService, StoryPointType } from '@metad/story/core' -import { nanoid } from 'nanoid' +import { NxStoryService } from '@metad/story/core' import { NGXLogger } from 'ngx-logger' -import { firstValueFrom } from 'rxjs' -import { StoryWidgetSchema } from './schema' -import { StoryPageSchema } from './schema/page.schema' export function injectStoryPageCommand(storyService: NxStoryService) { const logger = inject(NGXLogger) @@ -37,63 +33,40 @@ ${calcEntityTypePrompt(defaultCube)} return prompt }, actions: [ -// injectMakeCopilotActionable({ -// name: 'pick_default_cube', -// description: 'Pick a default cube', -// argumentAnnotations: [], -// implementation: async () => { -// const result = await storyService.openDefultDataSettings() -// logger.debug(`Pick the default cube is:`, result) -// if (result?.dataSource && result?.entities[0]) { -// dataSourceName = result.dataSource -// const entityType = await firstValueFrom(storyService.selectEntityType({dataSource: result.dataSource, entitySet: result.entities[0]})) -// defaultCube = entityType -// } -// return { -// id: nanoid(), -// role: 'function', -// content: `The cube is: -// \`\`\` -// ${calcEntityTypePrompt(defaultCube)} -// \`\`\` -// ` -// } -// } -// }), -// injectMakeCopilotActionable({ -// name: 'new_story_page', -// description: '', -// argumentAnnotations: [ -// { -// name: 'page', -// type: 'object', -// description: 'Page config', -// properties: zodToProperties(StoryPageSchema), -// required: true -// }, -// { -// name: 'widgets', -// type: 'array', -// description: 'Widgets in page config', -// items: { -// type: 'object', -// properties: zodToProperties(StoryWidgetSchema) -// }, -// required: true -// } -// ], -// implementation: async (page, widgets) => { -// logger.debug(`Function calling 'new_story_page', params is:`, page, widgets) -// storyService.newStoryPage({ -// ...page, -// type: StoryPointType.Canvas, -// // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) -// }) -// return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { -// Default: 'Instruction Execution Complete' -// })}` -// } -// }) + // injectMakeCopilotActionable({ + // name: 'new_story_page', + // description: '', + // argumentAnnotations: [ + // { + // name: 'page', + // type: 'object', + // description: 'Page config', + // properties: zodToProperties(StoryPageSchema), + // required: true + // }, + // { + // name: 'widgets', + // type: 'array', + // description: 'Widgets in page config', + // items: { + // type: 'object', + // properties: zodToProperties(StoryWidgetSchema) + // }, + // required: true + // } + // ], + // implementation: async (page, widgets) => { + // logger.debug(`Function calling 'new_story_page', params is:`, page, widgets) + // storyService.newStoryPage({ + // ...page, + // type: StoryPointType.Canvas, + // // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) + // }) + // return `✅ ${storyService.translate('Story.Copilot.InstructionExecutionComplete', { + // Default: 'Instruction Execution Complete' + // })}` + // } + // }) ] }) } diff --git a/libs/story-angular/story/copilot/page/command.ts b/libs/story-angular/story/copilot/page/command.ts new file mode 100644 index 000000000..6aac7018e --- /dev/null +++ b/libs/story-angular/story/copilot/page/command.ts @@ -0,0 +1,27 @@ +import { inject } from '@angular/core' +import { CopilotAgentType } from '@metad/copilot' +import { injectCopilotCommand } from '@metad/copilot-angular' +import { NxStoryService } from '@metad/story/core' +import { NGXLogger } from 'ngx-logger' +import { injectCreatePageAgent } from './graph' +import { STORY_PAGE_COMMAND_NAME } from './types' +import { TranslateService } from '@ngx-translate/core' + +export function injectStoryPageCommand() { + const logger = inject(NGXLogger) + const translate = inject(TranslateService) + const storyService = inject(NxStoryService) + + const createGraph = injectCreatePageAgent() + + return injectCopilotCommand(STORY_PAGE_COMMAND_NAME, { + alias: 'p', + description: translate.instant('Story.Copilot.CommandPageDesc', {Default: 'Describe the page you want'}), + agent: { + type: CopilotAgentType.Graph, + conversation: true, + interruptBefore: ['tools'] + }, + createGraph + }) +} diff --git a/libs/story-angular/story/copilot/page/graph.ts b/libs/story-angular/story/copilot/page/graph.ts new file mode 100644 index 000000000..e4e21e97c --- /dev/null +++ b/libs/story-angular/story/copilot/page/graph.ts @@ -0,0 +1,43 @@ +import { inject } from '@angular/core' +import { SystemMessage } from '@langchain/core/messages' +import { SystemMessagePromptTemplate } from '@langchain/core/prompts' +import { CreateGraphOptions, createReactAgent } from '@metad/copilot' +import { NxStoryService } from '@metad/story/core' +import { NGXLogger } from 'ngx-logger' +import { pageAgentState } from './types' +import { injectCreatePageTools } from './tools' + +export function injectCreatePageAgent() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + const tools = injectCreatePageTools() + + return async ({ llm, interruptBefore, interruptAfter }: CreateGraphOptions) => { + + return createReactAgent({ + state: pageAgentState, + llm, + interruptBefore, + interruptAfter, + tools: [...tools], + messageModifier: async (state) => { + const systemTemplate = `You are a BI analysis expert. +{{role}} +{{language}} + +Step 1. Create a new page in story dashboard. +Step 2. 根据提供的 Cube context 和分析主题逐个向 dashboard 中添加 widgets. + +Widget 类型分为 FilterBar, InputControl, Table, Chart, and KPI + +The cube context: +{{context}} +` + const system = await SystemMessagePromptTemplate.fromTemplate(systemTemplate, { + templateFormat: 'mustache' + }).format(state) + return [new SystemMessage(system), ...state.messages] + } + }) + } +} diff --git a/libs/story-angular/story/copilot/page/index.ts b/libs/story-angular/story/copilot/page/index.ts new file mode 100644 index 000000000..efb90b590 --- /dev/null +++ b/libs/story-angular/story/copilot/page/index.ts @@ -0,0 +1 @@ +export * from './command' \ No newline at end of file diff --git a/libs/story-angular/story/copilot/page/tools.ts b/libs/story-angular/story/copilot/page/tools.ts new file mode 100644 index 000000000..98be625c0 --- /dev/null +++ b/libs/story-angular/story/copilot/page/tools.ts @@ -0,0 +1,48 @@ +import { inject } from '@angular/core' +import { DynamicStructuredTool } from '@langchain/core/tools' +import { NxStoryService, StoryPointType, WidgetComponentType } from '@metad/story/core' +import { GridsterConfig } from 'angular-gridster2' +import { z } from 'zod' +import { createWidgetSchema, StoryPageSchema } from '../schema' +import { MinCols, MinRows } from './types' + +export function injectCreatePageTools() { + const storyService = inject(NxStoryService) + + return [ + new DynamicStructuredTool({ + name: 'createPage', + description: 'Create a new page in story dashboard.', + schema: z.object({ + page: StoryPageSchema + }), + func: async ({ page }) => { + storyService.newStoryPage({ + ...page, + type: StoryPointType.Canvas, + gridOptions: { + gridType: 'fit', + minCols: MinCols, + minRows: MinRows + } as GridsterConfig + // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) + }) + return `The new page be created!` + } + }), + new DynamicStructuredTool({ + name: 'createWidget', + description: 'Create a widget in story dashboard page.', + schema: createWidgetSchema({}), + func: async ({ title, position }) => { + storyService.createStoryWidget({ + component: WidgetComponentType.AnalyticalCard, + title, + position + }) + + return `The new widget be created!` + } + }) + ] +} diff --git a/libs/story-angular/story/copilot/page/types.ts b/libs/story-angular/story/copilot/page/types.ts new file mode 100644 index 000000000..b5e4978ff --- /dev/null +++ b/libs/story-angular/story/copilot/page/types.ts @@ -0,0 +1,13 @@ +import { StateGraphArgs } from '@langchain/langgraph/web' +import { AgentState, createCopilotAgentState } from '@metad/copilot' + +export const STORY_PAGE_COMMAND_NAME = 'page' + +export type PageAgentState = AgentState +export const pageAgentState: StateGraphArgs['channels'] = { + ...createCopilotAgentState() +} + + +export const MinCols = 20 +export const MinRows = 20 \ No newline at end of file diff --git a/libs/story-angular/story/copilot/schema/page.schema.ts b/libs/story-angular/story/copilot/schema/page.schema.ts index 500b8a2e7..591848020 100644 --- a/libs/story-angular/story/copilot/schema/page.schema.ts +++ b/libs/story-angular/story/copilot/schema/page.schema.ts @@ -3,4 +3,7 @@ import { z } from 'zod' export const StoryPageSchema = z.object({ name: z.string().describe(`The page title of story`), description: z.string().describe(`The page description of story`), + gridOptions: z.object({ + + }) }) From 8de3e74eadbcb069ceec4ea9fecaf0b197d7df61 Mon Sep 17 00:00:00 2001 From: meta-d Date: Fri, 26 Jul 2024 19:02:36 +0800 Subject: [PATCH 28/53] feat: story page agent --- .../src/app/features/story/copilot/index.ts | 3 +- .../features}/story/copilot/page/command.ts | 10 +- .../app/features}/story/copilot/page/graph.ts | 13 +- .../app/features}/story/copilot/page/index.ts | 0 .../app/features/story/copilot/page/tools.ts | 69 +++++++++ .../app/features}/story/copilot/page/types.ts | 0 .../src/app/features/story/copilot/tools.ts | 137 +++++++++++++++++- .../features/story/story/story.component.ts | 3 +- .../src/app/features/story/toolbar/types.ts | 2 +- apps/cloud/src/app/features/story/widgets.ts | 2 +- .../src/lib/copilot/cube.schema.ts | 4 + libs/story-angular/core/types.ts | 5 +- libs/story-angular/story/copilot/index.ts | 3 +- .../story-angular/story/copilot/page/tools.ts | 48 ------ .../story/copilot/schema/page.schema.ts | 3 +- .../filter-bar/filter-bar.component.html | 130 +++++++++-------- .../filter-bar/filter-bar.component.ts | 30 ++-- .../widgets/kpi/kpi.component.html | 132 ++++++++--------- .../widgets/kpi/kpi.component.ts | 4 +- .../member-tree/member-tree.component.html | 11 ++ .../member-tree/member-tree.component.ts | 7 +- .../property-select.component.html | 3 +- .../property-select.component.scss | 5 + .../property-select.component.ts | 6 +- .../src/lib/chat/chat.component.html | 4 +- .../src/lib/chat/chat.component.ts | 6 +- 26 files changed, 426 insertions(+), 214 deletions(-) rename {libs/story-angular => apps/cloud/src/app/features}/story/copilot/page/command.ts (68%) rename {libs/story-angular => apps/cloud/src/app/features}/story/copilot/page/graph.ts (70%) rename {libs/story-angular => apps/cloud/src/app/features}/story/copilot/page/index.ts (100%) create mode 100644 apps/cloud/src/app/features/story/copilot/page/tools.ts rename {libs/story-angular => apps/cloud/src/app/features}/story/copilot/page/types.ts (100%) delete mode 100644 libs/story-angular/story/copilot/page/tools.ts diff --git a/apps/cloud/src/app/features/story/copilot/index.ts b/apps/cloud/src/app/features/story/copilot/index.ts index fe230e49b..72e86a17c 100644 --- a/apps/cloud/src/app/features/story/copilot/index.ts +++ b/apps/cloud/src/app/features/story/copilot/index.ts @@ -1,3 +1,4 @@ export * from './calculation/index' export * from './schema' -export * from './story/index' \ No newline at end of file +export * from './story/index' +export * from './page/index' \ No newline at end of file diff --git a/libs/story-angular/story/copilot/page/command.ts b/apps/cloud/src/app/features/story/copilot/page/command.ts similarity index 68% rename from libs/story-angular/story/copilot/page/command.ts rename to apps/cloud/src/app/features/story/copilot/page/command.ts index 6aac7018e..55807462b 100644 --- a/libs/story-angular/story/copilot/page/command.ts +++ b/apps/cloud/src/app/features/story/copilot/page/command.ts @@ -2,10 +2,11 @@ import { inject } from '@angular/core' import { CopilotAgentType } from '@metad/copilot' import { injectCopilotCommand } from '@metad/copilot-angular' import { NxStoryService } from '@metad/story/core' +import { TranslateService } from '@ngx-translate/core' +import { injectAgentFewShotTemplate, injectExampleRetriever } from 'apps/cloud/src/app/@core/copilot' import { NGXLogger } from 'ngx-logger' import { injectCreatePageAgent } from './graph' import { STORY_PAGE_COMMAND_NAME } from './types' -import { TranslateService } from '@ngx-translate/core' export function injectStoryPageCommand() { const logger = inject(NGXLogger) @@ -14,14 +15,19 @@ export function injectStoryPageCommand() { const createGraph = injectCreatePageAgent() + const examplesRetriever = injectExampleRetriever(STORY_PAGE_COMMAND_NAME, { k: 5, vectorStore: null }) + const fewShotPrompt = injectAgentFewShotTemplate(STORY_PAGE_COMMAND_NAME, { k: 1, vectorStore: null }) + return injectCopilotCommand(STORY_PAGE_COMMAND_NAME, { alias: 'p', - description: translate.instant('Story.Copilot.CommandPageDesc', {Default: 'Describe the page you want'}), + description: translate.instant('Story.Copilot.CommandPageDesc', { Default: 'Describe the page you want' }), agent: { type: CopilotAgentType.Graph, conversation: true, interruptBefore: ['tools'] }, + examplesRetriever, + fewShotPrompt, createGraph }) } diff --git a/libs/story-angular/story/copilot/page/graph.ts b/apps/cloud/src/app/features/story/copilot/page/graph.ts similarity index 70% rename from libs/story-angular/story/copilot/page/graph.ts rename to apps/cloud/src/app/features/story/copilot/page/graph.ts index e4e21e97c..69472aaab 100644 --- a/libs/story-angular/story/copilot/page/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/page/graph.ts @@ -2,15 +2,17 @@ import { inject } from '@angular/core' import { SystemMessage } from '@langchain/core/messages' import { SystemMessagePromptTemplate } from '@langchain/core/prompts' import { CreateGraphOptions, createReactAgent } from '@metad/copilot' -import { NxStoryService } from '@metad/story/core' import { NGXLogger } from 'ngx-logger' import { pageAgentState } from './types' import { injectCreatePageTools } from './tools' +import { injectCreateFilterBarTool, injectCreateKPITool, injectCreateVariableTool } from '../tools' export function injectCreatePageAgent() { const logger = inject(NGXLogger) - const storyService = inject(NxStoryService) const tools = injectCreatePageTools() + const createFilterBar = injectCreateFilterBarTool() + const createKPI = injectCreateKPITool() + const createVariable = injectCreateVariableTool() return async ({ llm, interruptBefore, interruptAfter }: CreateGraphOptions) => { @@ -19,7 +21,7 @@ export function injectCreatePageAgent() { llm, interruptBefore, interruptAfter, - tools: [...tools], + tools: [...tools, createFilterBar, createKPI, createVariable], messageModifier: async (state) => { const systemTemplate = `You are a BI analysis expert. {{role}} @@ -28,7 +30,10 @@ export function injectCreatePageAgent() { Step 1. Create a new page in story dashboard. Step 2. 根据提供的 Cube context 和分析主题逐个向 dashboard 中添加 widgets. -Widget 类型分为 FilterBar, InputControl, Table, Chart, and KPI +Widget 类型分为 FilterBar, InputControl, Table, Chart, and KPI。 + +- 页面 layout 布局默认是 40 * 40. +- If there are variables in the cube, be sure to call 'createVariableControl' to create an input control widget for each variable to control the input value. The cube context: {{context}} diff --git a/libs/story-angular/story/copilot/page/index.ts b/apps/cloud/src/app/features/story/copilot/page/index.ts similarity index 100% rename from libs/story-angular/story/copilot/page/index.ts rename to apps/cloud/src/app/features/story/copilot/page/index.ts diff --git a/apps/cloud/src/app/features/story/copilot/page/tools.ts b/apps/cloud/src/app/features/story/copilot/page/tools.ts new file mode 100644 index 000000000..bee1b68f5 --- /dev/null +++ b/apps/cloud/src/app/features/story/copilot/page/tools.ts @@ -0,0 +1,69 @@ +import { inject } from '@angular/core' +import { DynamicStructuredTool } from '@langchain/core/tools' +import { NxStoryService, StoryPointType } from '@metad/story/core' +import { StoryPageSchema } from '@metad/story/story' +import { GridsterConfig } from 'angular-gridster2' +import { NGXLogger } from 'ngx-logger' +import { z } from 'zod' +import { MinCols, MinRows } from './types' + +export function injectCreatePageTools() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + return [ + new DynamicStructuredTool({ + name: 'createPage', + description: 'Create a new page in story dashboard.', + schema: z.object({ + page: StoryPageSchema + }), + func: async ({ page }) => { + logger.debug(`Execute copilot action 'createPage':`, page) + storyService.newStoryPage({ + ...page, + type: StoryPointType.Canvas, + gridOptions: { + gridType: 'fit', + minCols: page.gridOptions?.columns ?? MinCols, + minRows: page.gridOptions?.rows ?? MinRows + } as GridsterConfig + }) + return `The new page be created!` + } + }) + // new DynamicStructuredTool({ + // name: 'createWidget', + // description: 'Create a widget in story dashboard page.', + // schema: z.object({ + // dataSettings: DataSettingsSchema, + // widget: createWidgetSchema({ + // component: z + // .enum([ + // WidgetComponentType.AnalyticalCard, + // WidgetComponentType.AnalyticalGrid, + // WidgetComponentType.InputControl, + // WidgetComponentType.KpiCard, + // WidgetComponentType.FilterBar + // ]) + // .describe('The component type of widget') + // }) + // }), + // func: async ({ dataSettings, widget }) => { + // logger.debug( + // `Execute copilot action 'createWidget': '${widget.component}' using dataSettings:`, + // dataSettings, + // `widget:`, + // widget + // ) + // storyService.createStoryWidget({ + // ...widget, + // dataSettings, + // component: widget.component || WidgetComponentType.AnalyticalCard + // }) + + // return `The new widget be created!` + // } + // }) + ] +} diff --git a/libs/story-angular/story/copilot/page/types.ts b/apps/cloud/src/app/features/story/copilot/page/types.ts similarity index 100% rename from libs/story-angular/story/copilot/page/types.ts rename to apps/cloud/src/app/features/story/copilot/page/types.ts diff --git a/apps/cloud/src/app/features/story/copilot/tools.ts b/apps/cloud/src/app/features/story/copilot/tools.ts index 763d3d074..d9e95cb09 100644 --- a/apps/cloud/src/app/features/story/copilot/tools.ts +++ b/apps/cloud/src/app/features/story/copilot/tools.ts @@ -1,9 +1,11 @@ import { inject } from '@angular/core' import { MatDialog } from '@angular/material/dialog' import { DynamicStructuredTool } from '@langchain/core/tools' -import { markdownEntityType } from '@metad/core' +import { DataSettingsSchema, DimensionSchema, markdownEntityType, MeasureSchema, VariableSchema } from '@metad/core' import { NgmDSCoreService } from '@metad/ocap-angular/core' -import { NxStoryService } from '@metad/story/core' +import { omit } from '@metad/ocap-core' +import { FilterControlType, NxStoryService, WidgetComponentType } from '@metad/story/core' +import { createWidgetSchema } from '@metad/story/story' import { NGXLogger } from 'ngx-logger' import { firstValueFrom } from 'rxjs' import z from 'zod' @@ -37,3 +39,134 @@ export function injectPickCubeTool() { return pickCubeTool } + + +export function injectCreateFilterBarTool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + return new DynamicStructuredTool({ + name: 'createFilterBar', + description: 'Create a filter bar widget in story dashboard.', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema({ + dimensions: z.array(DimensionSchema), + }) + }), + func: async ({ dataSettings, widget }) => { + logger.debug( + `Execute copilot action 'createFilterBar' using dataSettings:`, + dataSettings, + `widget:`, + widget + ) + try { + storyService.createStoryWidget({ + ...omit(widget, 'dimensions'), + dataSettings: { + ...dataSettings, + selectionFieldsAnnotation: { + propertyPaths: widget.dimensions + } + }, + component: WidgetComponentType.FilterBar + }) + } catch (error: any) { + return `Error while creating the filter bar: ${error.message}` + } + + return `The new filter bar widget has been created!` + } + }) +} + + +export function injectCreateKPITool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + return new DynamicStructuredTool({ + name: 'createKPI', + description: 'Create a KPI widget in story dashboard.', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema({ + kpiValue: MeasureSchema, + kpiTarget: MeasureSchema.optional() + }) + }), + func: async ({ dataSettings, widget }) => { + logger.debug( + `Execute copilot action 'createKPI' using dataSettings:`, + dataSettings, + `widget:`, + widget + ) + try { + storyService.createStoryWidget({ + ...omit(widget, 'kpiValue', 'kpiTarget'), + dataSettings: { + ...dataSettings, + KPIAnnotation: { + DataPoint: { + Value: widget.kpiValue, + TargetValue: widget.kpiTarget + } + } + }, + component: WidgetComponentType.KpiCard + }) + } catch (error: any) { + return `Error while creating the kpi widget: ${error.message}` + } + + return `The new kpi widget has been created!` + } + }) +} + + +export function injectCreateVariableTool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + return new DynamicStructuredTool({ + name: 'createVariableControl', + description: 'Create a input control widget for cube variable', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema({ + variable: VariableSchema.describe('variable'), + }) + }), + func: async ({ dataSettings, widget }) => { + logger.debug( + `Execute copilot action 'createVariableControl' using dataSettings:`, + dataSettings, + `widget:`, + widget + ) + + try { + storyService.createStoryWidget({ + ...omit(widget, 'variable'), + dataSettings: { + ...dataSettings, + dimension: { + dimension: widget.variable?.variable + } + }, + options: { + controlType: FilterControlType.DropDownList + }, + component: WidgetComponentType.InputControl + }) + } catch (error: any) { + return `Error while creating the input control: ${error.message}` + } + + return `The new input control widget has been created!` + } + }) +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/story/story.component.ts b/apps/cloud/src/app/features/story/story/story.component.ts index 703f3ce31..d42121da5 100644 --- a/apps/cloud/src/app/features/story/story/story.component.ts +++ b/apps/cloud/src/app/features/story/story/story.component.ts @@ -34,7 +34,6 @@ import { } from '@metad/story/core' import { NxDesignerModule, NxSettingsPanelService } from '@metad/story/designer' import { - injectStoryPageCommand, injectStoryStyleCommand, injectStoryWidgetCommand, NxStoryComponent, @@ -55,7 +54,7 @@ import { StoryToolbarService } from '../toolbar/toolbar.service' import { ResponsiveBreakpoints, ResponsiveBreakpointType } from '../types' import { NgmCalculationEditorComponent } from '@metad/ocap-angular/entity' import { MatDialog } from '@angular/material/dialog' -import { injectCalculationGraphCommand, injectStoryCommand } from '../copilot' +import { injectCalculationGraphCommand, injectStoryCommand, injectStoryPageCommand } from '../copilot' @Component({ standalone: true, diff --git a/apps/cloud/src/app/features/story/toolbar/types.ts b/apps/cloud/src/app/features/story/toolbar/types.ts index 7f00b4850..721e975e7 100644 --- a/apps/cloud/src/app/features/story/toolbar/types.ts +++ b/apps/cloud/src/app/features/story/toolbar/types.ts @@ -16,7 +16,7 @@ export const COMPONENTS: { label: 'Filter Bar', icon: 'filter_alt', value: { - component: WidgetComponentType.StoryFilterBar + component: WidgetComponentType.FilterBar } }, { diff --git a/apps/cloud/src/app/features/story/widgets.ts b/apps/cloud/src/app/features/story/widgets.ts index 35592e8b2..1fdb71599 100644 --- a/apps/cloud/src/app/features/story/widgets.ts +++ b/apps/cloud/src/app/features/story/widgets.ts @@ -172,7 +172,7 @@ export const STORY_DESIGNER_COMPONENTS = [ { provide: STORY_DESIGNER_COMPONENT, useValue: { - type: WidgetComponentType.StoryFilterBar + '/Style', + type: WidgetComponentType.FilterBar + '/Style', component: NxComponentSettingsComponent, schema: WidgetStylingSchema }, diff --git a/libs/core-angular/src/lib/copilot/cube.schema.ts b/libs/core-angular/src/lib/copilot/cube.schema.ts index 265903daa..96531f361 100644 --- a/libs/core-angular/src/lib/copilot/cube.schema.ts +++ b/libs/core-angular/src/lib/copilot/cube.schema.ts @@ -81,6 +81,10 @@ export const CalculationSchema = z.object({ }).optional().describe('The formatting config of this measure') }) +export const VariableSchema = z.object({ + variable: z.string().describe('The name of the variable'), +}) + /** * Due to the instability of the AI's returned results, it is necessary to attempt to fix dimensions for different situations: * The dimensional attributes returned by AI may be level, hierarchy or dimension. diff --git a/libs/story-angular/core/types.ts b/libs/story-angular/core/types.ts index f401287c7..6dbad87ba 100644 --- a/libs/story-angular/core/types.ts +++ b/libs/story-angular/core/types.ts @@ -479,7 +479,8 @@ export interface StoryFeed extends Partial { export enum WidgetComponentType { AnalyticalCard = 'AnalyticalCard', AnalyticalGrid = 'AnalyticalGrid', - StoryFilterBar = 'StoryFilterBar', + // StoryFilterBar = 'StoryFilterBar', + FilterBar = 'FilterBar', KpiCard = 'KpiCard', InputControl = 'InputControl', AnalyticalGeography = 'AnalyticalGeography', @@ -493,7 +494,7 @@ export enum WidgetComponentType { export enum ComponentSettingsType { Story = 'Story', - StoryFilterBar = 'StoryFilterBar', + StoryFilterBar = WidgetComponentType.FilterBar, FilterBarField = 'FilterBarField', LinkedAnalysis = 'LinkedAnalysis', StoryPoint = 'StoryPoint', diff --git a/libs/story-angular/story/copilot/index.ts b/libs/story-angular/story/copilot/index.ts index 831fc7f40..b93361451 100644 --- a/libs/story-angular/story/copilot/index.ts +++ b/libs/story-angular/story/copilot/index.ts @@ -2,5 +2,4 @@ export * from './measure.command' export * from './schema/' export * from './style.command' export * from './types' -export * from './widget/index' -export * from './page/index' \ No newline at end of file +export * from './widget/index' \ No newline at end of file diff --git a/libs/story-angular/story/copilot/page/tools.ts b/libs/story-angular/story/copilot/page/tools.ts deleted file mode 100644 index 98be625c0..000000000 --- a/libs/story-angular/story/copilot/page/tools.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { inject } from '@angular/core' -import { DynamicStructuredTool } from '@langchain/core/tools' -import { NxStoryService, StoryPointType, WidgetComponentType } from '@metad/story/core' -import { GridsterConfig } from 'angular-gridster2' -import { z } from 'zod' -import { createWidgetSchema, StoryPageSchema } from '../schema' -import { MinCols, MinRows } from './types' - -export function injectCreatePageTools() { - const storyService = inject(NxStoryService) - - return [ - new DynamicStructuredTool({ - name: 'createPage', - description: 'Create a new page in story dashboard.', - schema: z.object({ - page: StoryPageSchema - }), - func: async ({ page }) => { - storyService.newStoryPage({ - ...page, - type: StoryPointType.Canvas, - gridOptions: { - gridType: 'fit', - minCols: MinCols, - minRows: MinRows - } as GridsterConfig - // widgets: widgets.map((item) => schemaToWidget(item, dataSourceName, defaultCube)) - }) - return `The new page be created!` - } - }), - new DynamicStructuredTool({ - name: 'createWidget', - description: 'Create a widget in story dashboard page.', - schema: createWidgetSchema({}), - func: async ({ title, position }) => { - storyService.createStoryWidget({ - component: WidgetComponentType.AnalyticalCard, - title, - position - }) - - return `The new widget be created!` - } - }) - ] -} diff --git a/libs/story-angular/story/copilot/schema/page.schema.ts b/libs/story-angular/story/copilot/schema/page.schema.ts index 591848020..b9bec25f7 100644 --- a/libs/story-angular/story/copilot/schema/page.schema.ts +++ b/libs/story-angular/story/copilot/schema/page.schema.ts @@ -4,6 +4,7 @@ export const StoryPageSchema = z.object({ name: z.string().describe(`The page title of story`), description: z.string().describe(`The page description of story`), gridOptions: z.object({ - + columns: z.number().describe('Number of columns of layout'), + rows: z.number().describe('Number of rows of layout'), }) }) diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.html b/libs/story-angular/widgets/filter-bar/filter-bar.component.html index a71850fb1..755e8a9b2 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.html +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.html @@ -1,76 +1,78 @@
+ @if (enabledToday()) { + + } - - + @for (control of controls$ | async; track control.name) { +
-
+ + + + - - - - + + - - + - - + - - - - -
+ + +
+ } diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts index d5d404cf3..072b8a693 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts @@ -9,7 +9,9 @@ import { OnDestroy, Output, ViewContainerRef, - inject + computed, + inject, + signal } from '@angular/core' import { FormBuilder, FormControl } from '@angular/forms' import { MatDialog } from '@angular/material/dialog' @@ -100,7 +102,7 @@ export class NxSmartFilterBarComponent // filters form = new FormBuilder().group({}) today = new FormControl() - selected: string + // selected: string get selectionFieldsAnnotation() { return this.dataSettings?.selectionFieldsAnnotation @@ -118,7 +120,7 @@ export class NxSmartFilterBarComponent ]).pipe( map(([dataSettings, entityType]) => { const selectionFields = dataSettings.selectionFieldsAnnotation - if (!selectionFields) { + if (!selectionFields?.propertyPaths) { return [] } @@ -143,14 +145,14 @@ export class NxSmartFilterBarComponent }) }), distinctUntilChanged(isEqual), - takeUntilDestroyed(this.destroyRef), + takeUntilDestroyed(), shareReplay(1) ) // 合并字段 Options public readonly controls$ = combineLatest([ this._controls$, - this.options$.pipe(distinctUntilChanged(isEqual)), + this.options$.pipe(distinctUntilChanged(isEqual), startWith(null)), this.form.valueChanges.pipe( filter(() => this.options?.cascadingEffect), distinctUntilChanged(isEqual), @@ -242,6 +244,14 @@ export class NxSmartFilterBarComponent return Array.from(this._controlsLoading.values()).includes(true) } + /** + |-------------------------------------------------------------------------- + | Signals + |-------------------------------------------------------------------------- + */ + readonly enabledToday = computed(() => this.optionsSignal()?.today?.enable) + readonly selectedField = signal(null) + /** |-------------------------------------------------------------------------- | Subscriptions (effect) @@ -340,7 +350,7 @@ export class NxSmartFilterBarComponent event.preventDefault() } - this.selected = name + this.selectedField.set(name) return this.settingsService ?.openDesigner( ComponentSettingsType.FilterBarField, @@ -361,9 +371,9 @@ export class NxSmartFilterBarComponent readonly saveAsDefaultMembers = this.updater( (state) => { - console.log(this.form.value) - state.options.filters = state.options.filters || {} - Object.keys(this.form.value).forEach((key) => { + state.options ??= {} + state.options.filters ??= {} + Object.keys(this.form.value ?? {}).forEach((key) => { const value = this.form.value[key] if (value?.members) { state.options.filters[key] = state.options.filters[key] ?? {} as FilterBarFieldOptions @@ -380,7 +390,7 @@ export class NxSmartFilterBarComponent @HostListener('click', ['$event']) private handleClick(event) { - this.selected = null + this.selectedField.set(null) } ngOnDestroy(): void { diff --git a/libs/story-angular/widgets/kpi/kpi.component.html b/libs/story-angular/widgets/kpi/kpi.component.html index f60174f62..3a2d34ada 100644 --- a/libs/story-angular/widgets/kpi/kpi.component.html +++ b/libs/story-angular/widgets/kpi/kpi.component.html @@ -1,70 +1,81 @@ -
-
- @if (options?.icon) { - {{options.icon}} - } - - +@if (error()) { +
+ 🐞 +
+ {{ error() }} +
+
+} @else { +
+
+ @if (options?.icon) { + {{options.icon}} + } + + - @if (kpiValue$ | async; as kpiValue) { -
- -
- } - - @if (trend$ | async; as trend) { -
-
-
- {{options?.targetText}} -
- - + @if (kpiValue$ | async; as kpiValue) { +
+
- - @if (options?.showDeviation) { -
-
- {{options?.deviationText || ('NX.SMART_KPI.DEVIATION_TEXT' | translate)}} + } + + @if (trend$ | async; as trend) { +
+
+
+ {{options?.targetText}}
- } - -
- } - @if (isLoading()) { - - } - -
+ @if (options?.showDeviation) { +
+
+ {{options?.deviationText || ('NX.SMART_KPI.DEVIATION_TEXT' | translate)}} +
+ + +
+ } + +
+ } - @if (additionalDataPoints$ | async; as additionals) { -
- @for (kpiValue of additionals; track $index) { - + @if (isLoading()) { + } +
- } -
+ + @if (additionalDataPoints$ | async; as additionals) { +
+ @for (kpiValue of additionals; track $index) { + + } +
+ } +
+}
- 🐞 -
- {{ error() }} -
-
-} - @if (editable && (placeholder$ | async)) {
diff --git a/libs/story-angular/widgets/kpi/kpi.component.ts b/libs/story-angular/widgets/kpi/kpi.component.ts index 92d52df0a..18aab3c6a 100644 --- a/libs/story-angular/widgets/kpi/kpi.component.ts +++ b/libs/story-angular/widgets/kpi/kpi.component.ts @@ -60,7 +60,7 @@ export class NxWidgetKpiComponent extends AbstractStoryWidget< public readonly trend$ = this.kpiValue$.pipe(filter((kpiValue) => !isNil(kpiValue?.arrow))) public readonly additionalDataPoints$ = this.dataService.additionalDataPoints$.pipe( map((kpiValues) => - kpiValues.map((kpiValue) => { + kpiValues?.map((kpiValue) => { switch (this.options?.additionalDataPoint?.value) { case 'Value': break @@ -82,7 +82,7 @@ export class NxWidgetKpiComponent extends AbstractStoryWidget< return kpiValue }) ), - map((additionals) => (additionals.length > 0 ? additionals : null)) + map((additionals) => (additionals?.length > 0 ? additionals : null)) ) /** diff --git a/packages/angular/controls/member-tree/member-tree.component.html b/packages/angular/controls/member-tree/member-tree.component.html index 744625c45..62d0b3c4a 100644 --- a/packages/angular/controls/member-tree/member-tree.component.html +++ b/packages/angular/controls/member-tree/member-tree.component.html @@ -38,3 +38,14 @@
+ +@if (error()) { +
+ 🐞 +
+ {{ error() }} +
+
+} \ No newline at end of file diff --git a/packages/angular/controls/member-tree/member-tree.component.ts b/packages/angular/controls/member-tree/member-tree.component.ts index be8491b64..91723de83 100644 --- a/packages/angular/controls/member-tree/member-tree.component.ts +++ b/packages/angular/controls/member-tree/member-tree.component.ts @@ -120,6 +120,8 @@ export class NgmMemberTreeComponent(null) + readonly slicer = computed(() => { let nodes = this.memberKeys() .map((key) => this.keyNodeMap.get(key)) @@ -164,13 +166,13 @@ export class NgmMemberTreeComponent this.options()?.autoActiveFirst) + readonly initial = signal(true) /** |-------------------------------------------------------------------------- | Effects |-------------------------------------------------------------------------- */ - readonly initial = signal(true) readonly loadedTreeNodes = effect(() => this.initial.set(true), { allowSignalWrites: true }) readonly autoActiveFirstEffect = effect( () => { @@ -236,6 +238,9 @@ export class NgmMemberTreeComponent { this.loadingChanging.emit(loading) }) + private errorSub = this.smartFilterService.selectResult().pipe(map(({ error }) => error), takeUntilDestroyed()).subscribe((err) => { + this.error.set(err) + }) constructor(private smartFilterService: NgmSmartFilterService) { this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren) diff --git a/packages/angular/entity/property-select/property-select.component.html b/packages/angular/entity/property-select/property-select.component.html index 7234da53b..d8fc22a5d 100644 --- a/packages/angular/entity/property-select/property-select.component.html +++ b/packages/angular/entity/property-select/property-select.component.html @@ -65,7 +65,8 @@ [ngClass]="{ 'ngm-option__dimension': property.role === AggregationRole.dimension, 'ngm-option__hierarchy': property.role === AggregationRole.hierarchy, - 'ngm-option__level': property.role === AggregationRole.level + 'ngm-option__level': property.role === AggregationRole.level, + 'ngm-option__hidden': property.visible === false }" > diff --git a/packages/angular/entity/property-select/property-select.component.scss b/packages/angular/entity/property-select/property-select.component.scss index fc702a163..af4a171a0 100644 --- a/packages/angular/entity/property-select/property-select.component.scss +++ b/packages/angular/entity/property-select/property-select.component.scss @@ -61,5 +61,10 @@ $prefix-color: (#{$prefix}, color); .mat-mdc-option.ngm-option__measure { padding-left: 16px; } + + .ngm-option__hidden { + display: none; + visibility: hidden; + } } } \ No newline at end of file diff --git a/packages/angular/entity/property-select/property-select.component.ts b/packages/angular/entity/property-select/property-select.component.ts index 04f107508..cb0f6470e 100644 --- a/packages/angular/entity/property-select/property-select.component.ts +++ b/packages/angular/entity/property-select/property-select.component.ts @@ -44,7 +44,7 @@ import { } from '@metad/ocap-core' import { cloneDeep, includes, isEmpty, isEqual, isNil, isString, negate, pick, uniq } from 'lodash-es' import { BehaviorSubject, combineLatest, firstValueFrom, Observable, of } from 'rxjs' -import { distinctUntilChanged, filter, map, shareReplay, startWith, combineLatestWith, debounceTime, pairwise, switchMap } from 'rxjs/operators' +import { distinctUntilChanged, filter, map, shareReplay, startWith, combineLatestWith, debounceTime, switchMap } from 'rxjs/operators' import { MatSelect, MatSelectModule } from '@angular/material/select' import { getEntityMeasures, PropertyAttributes } from '@metad/ocap-core' import { DisplayDensity, NgmDSCoreService, NgmOcapCoreService } from '@metad/ocap-angular/core' @@ -297,6 +297,10 @@ export class NgmPropertySelectComponent implements ControlValueAccessor, AfterVi levels.forEach((level) => { options.push(level) }) + } else { + levels.forEach((level) => { + options.push({...level, visible: false}) + }) } }) }) diff --git a/packages/copilot-angular/src/lib/chat/chat.component.html b/packages/copilot-angular/src/lib/chat/chat.component.html index 265c1b026..6f26c6aad 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.html +++ b/packages/copilot-angular/src/lib/chat/chat.component.html @@ -451,8 +451,8 @@ (click)="repleaceContext(word.text, item)" >
- {{item.caption}} - {{item.key}} + {{item.caption}} + {{item.key}}
diff --git a/packages/copilot-angular/src/lib/chat/chat.component.ts b/packages/copilot-angular/src/lib/chat/chat.component.ts index ba18f980e..67fb36eb7 100644 --- a/packages/copilot-angular/src/lib/chat/chat.component.ts +++ b/packages/copilot-angular/src/lib/chat/chat.component.ts @@ -836,7 +836,11 @@ export class NgmCopilotChatComponent { } pickExample(text: string) { - this.promptControl.setValue(`${this.commandWord()} ${text}`, {emitEvent: false}) + let prompt = this.commandWord() + if (this.context()) { + prompt += ` @${this.context().uKey}` + } + this.promptControl.setValue(`${prompt} ${text}`, {emitEvent: false}) this.promptCompletion.set(null) } } From b5d7ef44a4882a152295a5a23cbf6ab7d402847e Mon Sep 17 00:00:00 2001 From: meta-d Date: Fri, 26 Jul 2024 20:48:34 +0800 Subject: [PATCH 29/53] feat: variable in filter bar --- .../src/lib/models/control-type.ts | 3 + .../filter-bar/filter-bar.component.html | 121 +++++++------ .../filter-bar/filter-bar.component.ts | 164 ++++++++++-------- .../widgets/filter-bar/filter-bar.schema.ts | 2 +- .../angular/controls/smart-filter.service.ts | 6 +- .../controls/variable/variable.component.ts | 3 +- .../src/lib/services/smart-filter.service.ts | 17 +- 7 files changed, 180 insertions(+), 136 deletions(-) diff --git a/libs/core-angular/src/lib/models/control-type.ts b/libs/core-angular/src/lib/models/control-type.ts index ec5691402..11f1c4dbd 100644 --- a/libs/core-angular/src/lib/models/control-type.ts +++ b/libs/core-angular/src/lib/models/control-type.ts @@ -1,3 +1,6 @@ +/** + * @deprecated use FilterControlType + */ export enum ControlType { auto = 'auto', /** diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.html b/libs/story-angular/widgets/filter-bar/filter-bar.component.html index 755e8a9b2..b64d39ae3 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.html +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.html @@ -15,62 +15,72 @@
- - - - - - - - - - - - - - - - + + } + @case (FilterControlType.DropDownList) { + + + } + @case (FilterControlType.TreeSelect) { + + + } + @case (FilterControlType.Variable) { + + } + @default { + + } + }
} @@ -89,7 +99,6 @@ diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts index 072b8a693..a6a0cd3af 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts @@ -3,20 +3,24 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, + effect, EventEmitter, HostBinding, HostListener, + inject, OnDestroy, Output, - ViewContainerRef, - computed, - inject, - signal + signal, + ViewContainerRef } from '@angular/core' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { FormBuilder, FormControl } from '@angular/forms' import { MatDialog } from '@angular/material/dialog' +import { AbstractStoryWidget, ControlType } from '@metad/core' import { SmartFilterOptions } from '@metad/ocap-angular/controls' import { NgmAppearance, NgmSmartBusinessService, NgmSmartFilterBarService } from '@metad/ocap-angular/core' +import { NgmAdvancedFilterComponent } from '@metad/ocap-angular/selection' import { cloneDeep, DataSettings, @@ -26,31 +30,19 @@ import { IAdvancedFilter, isEmpty, ISlicer, + isVariableProperty, MemberSource, + Property, Syntax, - TimeGranularity, + TimeGranularity } from '@metad/ocap-core' -import { - AbstractStoryWidget, - ControlType, -} from '@metad/core' import { ComponentSettingsType, FilterControlType } from '@metad/story/core' import { NxSettingsPanelService } from '@metad/story/designer' import { assign, compact, isEqual, merge, pick } from 'lodash-es' import { NGXLogger } from 'ngx-logger' import { BehaviorSubject, combineLatest, EMPTY, firstValueFrom, Observable } from 'rxjs' -import { - distinctUntilChanged, - filter, - map, - shareReplay, - startWith, - switchMap, - tap, -} from 'rxjs/operators' +import { distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators' import { CascadingEffect, FilterBarFieldOptions, ISmartFilterBarOptions } from './types' -import { takeUntilDestroyed } from '@angular/core/rxjs-interop' -import { NgmAdvancedFilterComponent } from '@metad/ocap-angular/selection' export interface NxFilterControl { required?: boolean @@ -58,6 +50,7 @@ export interface NxFilterControl { placeholder?: string dataSettings?: DataSettings dimension: Dimension + property?: Property name: string controlType?: ControlType | FilterControlType options: Partial< @@ -70,13 +63,12 @@ export interface NxFilterControl { appearance: NgmAppearance } - @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'pac-widget-filter-bar', templateUrl: './filter-bar.component.html', styleUrls: ['./filter-bar.component.scss'], - providers: [ NgmSmartBusinessService ], + providers: [NgmSmartBusinessService] }) export class NxSmartFilterBarComponent extends AbstractStoryWidget @@ -90,9 +82,9 @@ export class NxSmartFilterBarComponent protected readonly _dialog = inject(MatDialog) protected readonly _cdr = inject(ChangeDetectorRef) private readonly _viewContainerRef = inject(ViewContainerRef) - protected readonly smartFilterBarService? = inject(NgmSmartFilterBarService, {optional: true}) - protected readonly logger? = inject(NGXLogger, {optional: true}) - private readonly settingsService? = inject(NxSettingsPanelService, {optional: true}) + protected readonly smartFilterBarService? = inject(NgmSmartFilterBarService, { optional: true }) + protected readonly logger? = inject(NGXLogger, { optional: true }) + private readonly settingsService? = inject(NxSettingsPanelService, { optional: true }) @Output() go = new EventEmitter() @Output() loadingChanging = new EventEmitter() @@ -112,12 +104,9 @@ export class NxSmartFilterBarComponent map((dataSettings) => !(dataSettings?.dataSource && dataSettings?.entitySet)) ) private readonly entityType$ = this.dataService.selectEntityType() - public readonly syntax$ = this.entityType$.pipe(map(({syntax}) => syntax)) - - private readonly _controls$ = combineLatest([ - this.dataSettings$, - this.entityType$ - ]).pipe( + public readonly syntax$ = this.entityType$.pipe(map(({ syntax }) => syntax)) + + private readonly _controls$ = combineLatest([this.dataSettings$, this.entityType$]).pipe( map(([dataSettings, entityType]) => { const selectionFields = dataSettings.selectionFieldsAnnotation if (!selectionFields?.propertyPaths) { @@ -134,13 +123,18 @@ export class NxSmartFilterBarComponent }) .filter(({ field, property }) => !!field && !!property) .map(({ field, property }) => { + let controlType = null + if (isVariableProperty(property)) { + controlType = FilterControlType.Variable + } return { name: getPropertyName(field), dimension: field, label: property.caption || field.dimension, dataSettings: pick(dataSettings, 'dataSource', 'entitySet'), - options: { - } + controlType, + options: {}, + property } as NxFilterControl }) }), @@ -156,7 +150,7 @@ export class NxSmartFilterBarComponent this.form.valueChanges.pipe( filter(() => this.options?.cascadingEffect), distinctUntilChanged(isEqual), - startWith(null), + startWith(null) ) ]).pipe( map(([controls, options, slicers]) => { @@ -192,9 +186,13 @@ export class NxSmartFilterBarComponent controlType: controlOptions?.controlType ?? control.controlType, label: option.label || control.label, placeholder: option.placeholder || control.placeholder, - options: merge({ - autocomplete: true - }, control.options, option), + options: merge( + { + autocomplete: true + }, + control.options, + option + ), styling: controlOptions?.styling, appearance: this.styling?.appearance } as NxFilterControl @@ -252,6 +250,8 @@ export class NxSmartFilterBarComponent readonly enabledToday = computed(() => this.optionsSignal()?.today?.enable) readonly selectedField = signal(null) + readonly defaultSlicers = signal(null, { equal: isEqual }) + /** |-------------------------------------------------------------------------- | Subscriptions (effect) @@ -262,18 +262,34 @@ export class NxSmartFilterBarComponent private controlsSub = this._controls$.subscribe((controls) => { const options = this.options this.form.reset() - controls.map((filter: NxFilterControl) => { + const defaultSlicers = [] + controls.forEach((filter: NxFilterControl) => { if (!this.form.contains(filter.name)) { - const formCtrl = options?.filters?.[filter.name]?.options?.defaultMembers ? new FormControl({ - dimension: filter.dimension, - members: options.filters[filter.name].options.defaultMembers - }): new FormControl() + let value = null + if (options?.filters?.[filter.name]?.options?.defaultMembers) { + value = { + dimension: filter.dimension, + members: options.filters[filter.name].options.defaultMembers + } + defaultSlicers.push(value) + } + const formCtrl = new FormControl(value) this.form.setControl(filter.name, formCtrl) } }) + + this.defaultSlicers.set(defaultSlicers) }) + private defaultSlicersEffect = effect( + () => { + this.smartFilterBarService.change(this.defaultSlicers()) + this.onGo() + }, + { allowSignalWrites: true } + ) + ngAfterViewInit(): void { // 当前日期变化刷新数据 this.today.valueChanges.pipe(filter(() => this.options?.liveMode)).subscribe(() => this.onGo()) @@ -281,7 +297,12 @@ export class NxSmartFilterBarComponent combineLatest([this.form.valueChanges, this.combinationSlicer$]) .pipe( map(([values, combinationSlicer]: [{ [key: string]: ISlicer }, IAdvancedFilter]) => { - return compact([...Object.values(values).filter((slicer) => !isEmpty(slicer?.members)).map(cloneDeep), combinationSlicer]) + return compact([ + ...Object.values(values) + .filter((slicer) => !isEmpty(slicer?.members)) + .map(cloneDeep), + combinationSlicer + ]) }), takeUntilDestroyed(this.destroyRef) ) @@ -308,19 +329,20 @@ export class NxSmartFilterBarComponent async openCombinationSlicer() { const entityType = await firstValueFrom(this.entityType$) - const combinationSlicer = await firstValueFrom(this._dialog - .open(NgmAdvancedFilterComponent, { - viewContainerRef: this._viewContainerRef, - data: { - dataSettings: pick(this.dataSettings, 'dataSource', 'entitySet'), - entityType, - syntax: entityType.syntax, - advancedFilter: this.combinationSlicer$.value - } - }) - .afterClosed() + const combinationSlicer = await firstValueFrom( + this._dialog + .open(NgmAdvancedFilterComponent, { + viewContainerRef: this._viewContainerRef, + data: { + dataSettings: pick(this.dataSettings, 'dataSource', 'entitySet'), + entityType, + syntax: entityType.syntax, + advancedFilter: this.combinationSlicer$.value + } + }) + .afterClosed() ) - + // Cancel:="" Dismiss:=undefined if (combinationSlicer || combinationSlicer === null) { this.combinationSlicer$.next(combinationSlicer) @@ -351,13 +373,15 @@ export class NxSmartFilterBarComponent } this.selectedField.set(name) - return this.settingsService + return ( + this.settingsService ?.openDesigner( ComponentSettingsType.FilterBarField, this.options.filters?.[name] ?? {}, `${this.key}/${name}` ) .pipe(tap((options: any) => this.updateFieldOptions({ key: name, options }))) ?? EMPTY + ) }) ) }) @@ -369,20 +393,18 @@ export class NxSmartFilterBarComponent } ) - readonly saveAsDefaultMembers = this.updater( - (state) => { - state.options ??= {} - state.options.filters ??= {} - Object.keys(this.form.value ?? {}).forEach((key) => { - const value = this.form.value[key] - if (value?.members) { - state.options.filters[key] = state.options.filters[key] ?? {} as FilterBarFieldOptions - state.options.filters[key].options = state.options.filters[key].options ?? {} - state.options.filters[key].options.defaultMembers = value.members - } - }) - } - ) + readonly saveAsDefaultMembers = this.updater((state) => { + state.options ??= {} + state.options.filters ??= {} + Object.keys(this.form.value ?? {}).forEach((key) => { + const value = this.form.value[key] + if (value?.members) { + state.options.filters[key] = state.options.filters[key] ?? ({} as FilterBarFieldOptions) + state.options.filters[key].options = state.options.filters[key].options ?? {} + state.options.filters[key].options.defaultMembers = value.members + } + }) + }) onLoadingChanging(loading: boolean, name: string) { this._controlsLoading.set(name, loading) @@ -392,7 +414,7 @@ export class NxSmartFilterBarComponent private handleClick(event) { this.selectedField.set(null) } - + ngOnDestroy(): void { this.destroySubject$.next() this.destroySubject$.complete() diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.schema.ts b/libs/story-angular/widgets/filter-bar/filter-bar.schema.ts index 41947dccc..a82937347 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.schema.ts +++ b/libs/story-angular/widgets/filter-bar/filter-bar.schema.ts @@ -63,7 +63,7 @@ export class StoryFilterBarSchemaService ex sortable: true, dataSettings: this.dataSettings$, entityType: this.entityType$, - capacities: [PropertyCapacity.Dimension] + capacities: [PropertyCapacity.Dimension, PropertyCapacity.Parameter] } } } diff --git a/packages/angular/controls/smart-filter.service.ts b/packages/angular/controls/smart-filter.service.ts index 6b8e0e475..e04ec7111 100644 --- a/packages/angular/controls/smart-filter.service.ts +++ b/packages/angular/controls/smart-filter.service.ts @@ -1,11 +1,11 @@ import { Injectable, OnDestroy } from '@angular/core' -import { NgmDSCoreService } from '@metad/ocap-angular/core' +import { NgmDSCoreService, NgmSmartFilterBarService } from '@metad/ocap-angular/core' import { SmartFilterService } from '@metad/ocap-core' @Injectable() export class NgmSmartFilterService extends SmartFilterService implements OnDestroy { - constructor(dsCoreService: NgmDSCoreService) { - super(dsCoreService) + constructor(dsCoreService: NgmDSCoreService, ngmFilterBarService: NgmSmartFilterBarService) { + super(dsCoreService, ngmFilterBarService) } ngOnDestroy(): void { diff --git a/packages/angular/controls/variable/variable.component.ts b/packages/angular/controls/variable/variable.component.ts index d624c73c4..ea59c2853 100644 --- a/packages/angular/controls/variable/variable.component.ts +++ b/packages/angular/controls/variable/variable.component.ts @@ -99,7 +99,8 @@ export class NgmVariableComponent implements ControlValueAccessor { ...this.slicer, dimension: { dimension: variable.referenceDimension, - hierarchy: variable.referenceHierarchy + hierarchy: variable.referenceHierarchy, + parameter: variable.name, }, members: [member] } diff --git a/packages/core/src/lib/services/smart-filter.service.ts b/packages/core/src/lib/services/smart-filter.service.ts index badf37c5b..fc7ca738a 100644 --- a/packages/core/src/lib/services/smart-filter.service.ts +++ b/packages/core/src/lib/services/smart-filter.service.ts @@ -1,4 +1,4 @@ -import { combineLatest, distinctUntilChanged, filter, map, switchMap, withLatestFrom } from 'rxjs' +import { combineLatest, distinctUntilChanged, filter, map, Observable, switchMap, withLatestFrom } from 'rxjs' import { hierarchize } from '../annotations' import { DSCoreService } from '../ds-core.service' import { @@ -7,9 +7,10 @@ import { getEntityHierarchy, IDimensionMember } from '../models' -import { C_MEASURES, Dimension, getPropertyHierarchy, IMember, QueryOptions } from '../types' +import { C_MEASURES, Dimension, getPropertyHierarchy, IMember, ISlicer, QueryOptions } from '../types' import { isEqual, isNil, uniqBy } from '../utils' import { SmartBusinessService, SmartBusinessState } from './smart-business.service' +import { SmartFilterBarService } from './smart-filter-bar.service' export enum TypeAheadType { @@ -110,8 +111,15 @@ export class SmartFilterService + + constructor(dsCoreService: DSCoreService, private _smartFilterBar: SmartFilterBarService) { super(dsCoreService) + + this.variables$ = this._smartFilterBar.onChange().pipe( + map((slicers) => slicers.filter((slicer) => !!slicer.dimension?.parameter)), + distinctUntilChanged(isEqual) + ) } override onInit() { @@ -196,7 +204,8 @@ export class SmartFilterService super.selectQuery({ ...options, filters })), map((result) => { const valueProperty = dimension.hierarchy || dimension.dimension const captionProperty = dimension.memberCaption || hProperty?.memberCaption From 53ea62180037ac6ae9fb3e492d5e71908d4a25df Mon Sep 17 00:00:00 2001 From: meta-d Date: Fri, 26 Jul 2024 21:21:23 +0800 Subject: [PATCH 30/53] feat: default members for filter bar variable --- .../copilot/basic/basic.component.html | 4 +- .../filter-bar/filter-bar.component.ts | 65 ++++++++++++------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html index eac9d7975..dc53e1c0d 100644 --- a/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html +++ b/apps/cloud/src/app/features/setting/copilot/basic/basic.component.html @@ -12,7 +12,7 @@ - +
@@ -81,7 +81,7 @@ - +
diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts index a6a0cd3af..530b53134 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts @@ -34,11 +34,12 @@ import { MemberSource, Property, Syntax, - TimeGranularity + TimeGranularity, + VariableProperty } from '@metad/ocap-core' import { ComponentSettingsType, FilterControlType } from '@metad/story/core' import { NxSettingsPanelService } from '@metad/story/designer' -import { assign, compact, isEqual, merge, pick } from 'lodash-es' +import { assign, compact, isEqual, merge, omit, pick } from 'lodash-es' import { NGXLogger } from 'ngx-logger' import { BehaviorSubject, combineLatest, EMPTY, firstValueFrom, Observable } from 'rxjs' import { distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators' @@ -211,26 +212,26 @@ export class NxSmartFilterBarComponent }) ) - private readonly defaultSlicers$ = this.options$.pipe( - map((options) => { - if (options?.filters) { - return Object.keys(options.filters).reduce((acc, key) => { - if (options.filters[key].options?.defaultMembers?.length) { - acc[key] = { - dimension: { - dimension: key - }, - members: options.filters[key].options.defaultMembers - } - } - return acc - }, {}) - } - - return null - }), - distinctUntilChanged(isEqual) - ) + // private readonly defaultSlicers$ = this.options$.pipe( + // map((options) => { + // if (options?.filters) { + // return Object.keys(options.filters).reduce((acc, key) => { + // if (options.filters[key].options?.defaultMembers?.length) { + // acc[key] = { + // dimension: { + // dimension: key + // }, + // members: options.filters[key].options.defaultMembers + // } + // } + // return acc + // }, {}) + // } + + // return null + // }), + // distinctUntilChanged(isEqual) + // ) /** * State for combination slicer @@ -271,6 +272,23 @@ export class NxSmartFilterBarComponent dimension: filter.dimension, members: options.filters[filter.name].options.defaultMembers } + } else if (filter.dimension.members?.length) { + if (filter.controlType === FilterControlType.Variable) { + value = { + dimension: { + dimension: (filter.property).referenceDimension, + hierarchy: (filter.property).referenceHierarchy + }, + members: filter.dimension.members + } + } else { + value = { + dimension: omit(filter.dimension, 'members'), + members: filter.dimension.members + } + } + } + if (value) { defaultSlicers.push(value) } const formCtrl = new FormControl(value) @@ -377,7 +395,7 @@ export class NxSmartFilterBarComponent this.settingsService ?.openDesigner( ComponentSettingsType.FilterBarField, - this.options.filters?.[name] ?? {}, + this.options?.filters?.[name] ?? {}, `${this.key}/${name}` ) .pipe(tap((options: any) => this.updateFieldOptions({ key: name, options }))) ?? EMPTY @@ -388,6 +406,7 @@ export class NxSmartFilterBarComponent readonly updateFieldOptions = this.updater( (state, { key, options }: { key: string; options: FilterBarFieldOptions }) => { + state.options ??= {} state.options.filters = state.options.filters || {} state.options.filters[key] = options } From 8b6fec23827dfbbbd9ad95bf1ffac03a7c80254f Mon Sep 17 00:00:00 2001 From: meta-d Date: Sat, 27 Jul 2024 00:25:29 +0800 Subject: [PATCH 31/53] feat: chart property schema --- .../analytical-card/analytical-card.schema.ts | 30 +-- .../chart-property.component.html | 196 +++++++++--------- .../chart-property.component.ts | 86 +++----- 3 files changed, 154 insertions(+), 158 deletions(-) diff --git a/libs/story-angular/widgets/analytical-card/analytical-card.schema.ts b/libs/story-angular/widgets/analytical-card/analytical-card.schema.ts index 7091ef1f2..bc0ee8a8d 100644 --- a/libs/story-angular/widgets/analytical-card/analytical-card.schema.ts +++ b/libs/story-angular/widgets/analytical-card/analytical-card.schema.ts @@ -1,5 +1,7 @@ import { inject, Injectable } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' import { ColorPalettes } from '@metad/core' +import { PropertyCapacity } from '@metad/ocap-angular/entity' import { ChartAnnotation, ChartOptions, @@ -39,8 +41,6 @@ import { TooltipCapacity, VisualMapCapacity } from './schemas' -import { toSignal } from '@angular/core/rxjs-interop' -import { PropertyCapacity } from '@metad/ocap-angular/entity' export interface AnalyticalCardSchemaState extends SchemaState { model: { @@ -103,7 +103,9 @@ export class AnalyticalCardSchemaService extends DataSettingsSchemaService i18n?.STYLING?.ECHARTS)), this.chartType$]).pipe( + return combineLatest([this.storyDesigner$.pipe(map((i18n) => i18n?.STYLING?.ECHARTS)), this.chartType$.pipe(distinctUntilChanged(isEqual))]).pipe( map(([ECHARTS, chartType]) => this.getChartOptions(chartType, ECHARTS).fieldGroup) ) } @@ -406,7 +408,7 @@ export class ChartOptionsSchemaService implements DesignerSchema { readonly title$ = of(`Chart options`) readonly title = toSignal(this.title$) - + get chartType() { return this.chartType$.value } @@ -422,11 +424,13 @@ export class ChartOptionsSchemaService implements DesignerSchema { } getSchema() { - return combineLatest([this.storyDesigner$.pipe(map((i18n) => i18n?.STYLING?.ECHARTS)), - this.chartType$.pipe(map((chartType) => omit(chartType, 'chartOptions')), distinctUntilChanged(isEqual)) - ]).pipe( - map(([ECHARTS, chartType]) => getChartOptionsSchema(chartType, ECHARTS).fieldGroup) - ) + return combineLatest([ + this.storyDesigner$.pipe(map((i18n) => i18n?.STYLING?.ECHARTS)), + this.chartType$.pipe( + map((chartType) => omit(chartType, 'chartOptions')), + distinctUntilChanged(isEqual) + ) + ]).pipe(map(([ECHARTS, chartType]) => getChartOptionsSchema(chartType, ECHARTS).fieldGroup)) } } diff --git a/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.html b/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.html index c872f1c6b..87b97000b 100644 --- a/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.html +++ b/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.html @@ -1,13 +1,13 @@
- @if (showChartAttributes && isDimension()) { + @if (showChartAttributes() && isDimension()) {
} + @if (showMeasureRole()) { +
+ -
- - - - - {{ 'Story.ChartProperty.' + role.label | translate: {Default: role.label} }} - - -
- -
- - - - - {{ 'Story.ChartProperty.None' | translate: {Default: 'None'} }} - - - {{ 'Story.ChartProperty.' + shape.label | translate: {Default: shape.label} }} - - -
- -
- - - - - {{ 'Story.ChartProperty.None' | translate: {Default: 'None'} }} - - - {{item.label}} - - -
- -
- - - + + + {{ 'Story.ChartProperty.' + role.label | translate: {Default: role.label} }} + + +
+ } + + @if (showMeasureShape()) { +
+ + + + + {{ 'Story.ChartProperty.None' | translate: {Default: 'None'} }} + + + {{ 'Story.ChartProperty.' + shape.label | translate: {Default: shape.label} }} + + +
+ } + @if (showMeasurePalettePattern()) { +
+ -
- - + + + {{ 'Story.ChartProperty.None' | translate: {Default: 'None'} }} + + + {{item.label}} + +
-
+ } - -
- -
- sort - {{ 'Story.ChartProperty.BarChart' | translate: {Default: 'Bar Chart'} }} + @if (showColorPalette()) { +
+ + + + +
+ +
- -
- +
+ } -
- + + @if (showMeasureGridBar()) { +
+ +
+ sort + {{ 'Story.ChartProperty.BarChart' | translate: {Default: 'Bar Chart'} }} +
+
+
+ } + + @if (showMeasureReferenceLine()) { +
+ - -
+ +
+ } - @if (showMeasureChartOptions) { + @if (showMeasureChartOptions()) {
- +
}
diff --git a/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.ts b/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.ts index 2ef44e8ac..5031f558e 100644 --- a/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.ts +++ b/libs/story-angular/widgets/analytical-card/chart-property/chart-property.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common' -import { Component, Input, computed, forwardRef, inject, signal } from '@angular/core' +import { Component, computed, forwardRef, inject, input, signal } from '@angular/core' import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop' import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' @@ -16,6 +16,7 @@ import { } from '@metad/components/palette' import { ColorPalettes, NxCoreService } from '@metad/core' import { AppearanceDirective, DensityDirective, NgmDSCoreService, NgmOcapCoreService } from '@metad/ocap-angular/core' +import { NgmEntityModule, PropertyCapacity } from '@metad/ocap-angular/entity' import { AggregationRole, CalculationProperty, @@ -31,11 +32,11 @@ import { } from '@metad/ocap-core' import { NxDesignerModule, NxSettingsPanelService } from '@metad/story/designer' import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { upperFirst } from 'lodash-es' import { BehaviorSubject, distinctUntilChanged, from, map } from 'rxjs' import { NgmChartDimensionComponent } from './chart-dimension.component' import { NgmChartMeasureComponent } from './chart-measure.component' import { NgmReferenceLineComponent } from './reference-line.component' -import { NgmEntityModule, PropertyCapacity } from '@metad/ocap-angular/entity' @Component({ standalone: true, @@ -79,26 +80,11 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { public settingsService? = inject(NxSettingsPanelService, { optional: true }) public translateService = inject(TranslateService) - @Input() label: string - @Input() capacities: PropertyCapacity[] - - @Input() get dataSettings(): DataSettings { - return this._dataSettings() - } - set dataSettings(value) { - this._dataSettings.set(value) - } - private readonly _dataSettings = signal(null) - - @Input() get entityType(): EntityType { - return this._entityType() - } - set entityType(value) { - this._entityType.set(value) - } - private readonly _entityType = signal(null) - - @Input() chartType: ChartType + readonly label = input(null) + readonly capacities = input(null) + readonly chartType = input(null) + readonly dataSettings = input(null) + readonly entityType = input(null) public interpolateGroups: NgmChromaticInterpolateGroup[] public colorPalettes = ColorPalettes @@ -148,35 +134,25 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { */ formControl = new FormControl() - get showMeasureStyle() { - return this.capacities?.includes(PropertyCapacity.MeasureStyle) - } - get showColorPalette() { - return this.capacities?.includes(PropertyCapacity.MeasureStylePalette) - } - get showMeasurePalettePattern() { - return this.capacities?.includes(PropertyCapacity.MeasureStylePalettePattern) - } - get showChartAttributes() { - return this.capacities?.includes(PropertyCapacity.DimensionChart) - } - get showMeasureRole() { - return this.capacities?.includes(PropertyCapacity.MeasureStyleRole) - } - get showMeasureShape() { - return this.capacities?.includes(PropertyCapacity.MeasureStyleShape) - } - get showMeasureGridBar() { - return this.capacities?.includes(PropertyCapacity.MeasureStyleGridBar) - } - get showMeasureReferenceLine() { - return this.capacities?.includes(PropertyCapacity.MeasureStyleReferenceLine) - } - get showMeasureChartOptions() { - return this.capacities?.includes(PropertyCapacity.MeasureStyleChartOptions) - } + // get showMeasureStyle() { + // return this.capacities()?.includes(PropertyCapacity.MeasureStyle) + // } + readonly showColorPalette = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureStylePalette)) + readonly showMeasurePalettePattern = computed(() => + this.capacities()?.includes(PropertyCapacity.MeasureStylePalettePattern) + ) + readonly showChartAttributes = computed(() => this.capacities()?.includes(PropertyCapacity.DimensionChart)) + readonly showMeasureRole = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureStyleRole)) + readonly showMeasureShape = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureStyleShape)) + readonly showMeasureGridBar = computed(() => this.capacities()?.includes(PropertyCapacity.MeasureStyleGridBar)) + readonly showMeasureReferenceLine = computed(() => + this.capacities()?.includes(PropertyCapacity.MeasureStyleReferenceLine) + ) + readonly showMeasureChartOptions = computed(() => + this.capacities()?.includes(PropertyCapacity.MeasureStyleChartOptions) + ) - public readonly syntax = computed(() => this.entityType?.syntax) + public readonly syntax = computed(() => this.entityType()?.syntax) public readonly restrictedDimensions$ = new BehaviorSubject(null) get role() { @@ -279,7 +255,7 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { ) ) - public readonly property = computed(() => getEntityProperty(this.entityType, this.dimension())) + public readonly property = computed(() => getEntityProperty(this.entityType(), this.dimension())) public readonly isDimension = computed(() => { return this.property()?.role === AggregationRole.dimension @@ -288,6 +264,11 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { return this.property()?.role === AggregationRole.measure }) + readonly _chartType = computed(() => ({ + ...(this.chartType() ?? {}), + type: upperFirst(this.model()?.shapeType ?? this.chartType()?.type) + })) + /** |-------------------------------------------------------------------------- | Subscriptions (effect) @@ -310,6 +291,7 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { ...(value ?? {}) }) }) + onChange: (input: any) => void onTouched: () => void @@ -340,7 +322,7 @@ export class NgmChartPropertyComponent implements ControlValueAccessor { onCalculationChange(property: CalculationProperty) { this.ocapService.updateEntity({ type: 'Calculation', - dataSettings: this.dataSettings, + dataSettings: this.dataSettings(), property }) } From 254d99dd74263b1c1219ff927b6155b5882ce4e2 Mon Sep 17 00:00:00 2001 From: meta-d Date: Sat, 27 Jul 2024 00:25:46 +0800 Subject: [PATCH 32/53] feat: filter bar --- .../filter-bar/filter-bar.component.ts | 50 ++++++------------- .../angular/controls/smart-filter.service.ts | 4 +- .../src/lib/services/smart-filter.service.ts | 8 +-- 3 files changed, 21 insertions(+), 41 deletions(-) diff --git a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts index 530b53134..f6a7e655b 100644 --- a/libs/story-angular/widgets/filter-bar/filter-bar.component.ts +++ b/libs/story-angular/widgets/filter-bar/filter-bar.component.ts @@ -212,27 +212,6 @@ export class NxSmartFilterBarComponent }) ) - // private readonly defaultSlicers$ = this.options$.pipe( - // map((options) => { - // if (options?.filters) { - // return Object.keys(options.filters).reduce((acc, key) => { - // if (options.filters[key].options?.defaultMembers?.length) { - // acc[key] = { - // dimension: { - // dimension: key - // }, - // members: options.filters[key].options.defaultMembers - // } - // } - // return acc - // }, {}) - // } - - // return null - // }), - // distinctUntilChanged(isEqual) - // ) - /** * State for combination slicer */ @@ -267,25 +246,26 @@ export class NxSmartFilterBarComponent controls.forEach((filter: NxFilterControl) => { if (!this.form.contains(filter.name)) { let value = null + let dimension = null + if (filter.controlType === FilterControlType.Variable) { + const variable = filter.property + dimension = { + dimension: variable.referenceDimension, + hierarchy: variable.referenceHierarchy, + parameter: variable.name + } + } else { + dimension = omit(filter.dimension, 'members') + } if (options?.filters?.[filter.name]?.options?.defaultMembers) { value = { - dimension: filter.dimension, + dimension, members: options.filters[filter.name].options.defaultMembers } } else if (filter.dimension.members?.length) { - if (filter.controlType === FilterControlType.Variable) { - value = { - dimension: { - dimension: (filter.property).referenceDimension, - hierarchy: (filter.property).referenceHierarchy - }, - members: filter.dimension.members - } - } else { - value = { - dimension: omit(filter.dimension, 'members'), - members: filter.dimension.members - } + value = { + dimension, + members: filter.dimension.members } } if (value) { diff --git a/packages/angular/controls/smart-filter.service.ts b/packages/angular/controls/smart-filter.service.ts index e04ec7111..28fea98a5 100644 --- a/packages/angular/controls/smart-filter.service.ts +++ b/packages/angular/controls/smart-filter.service.ts @@ -1,10 +1,10 @@ -import { Injectable, OnDestroy } from '@angular/core' +import { Injectable, OnDestroy, Optional } from '@angular/core' import { NgmDSCoreService, NgmSmartFilterBarService } from '@metad/ocap-angular/core' import { SmartFilterService } from '@metad/ocap-core' @Injectable() export class NgmSmartFilterService extends SmartFilterService implements OnDestroy { - constructor(dsCoreService: NgmDSCoreService, ngmFilterBarService: NgmSmartFilterBarService) { + constructor(dsCoreService: NgmDSCoreService, @Optional() ngmFilterBarService?: NgmSmartFilterBarService) { super(dsCoreService, ngmFilterBarService) } diff --git a/packages/core/src/lib/services/smart-filter.service.ts b/packages/core/src/lib/services/smart-filter.service.ts index fc7ca738a..d5517c894 100644 --- a/packages/core/src/lib/services/smart-filter.service.ts +++ b/packages/core/src/lib/services/smart-filter.service.ts @@ -1,4 +1,4 @@ -import { combineLatest, distinctUntilChanged, filter, map, Observable, switchMap, withLatestFrom } from 'rxjs' +import { combineLatest, distinctUntilChanged, filter, map, Observable, of, switchMap, withLatestFrom } from 'rxjs' import { hierarchize } from '../annotations' import { DSCoreService } from '../ds-core.service' import { @@ -113,13 +113,13 @@ export class SmartFilterService - constructor(dsCoreService: DSCoreService, private _smartFilterBar: SmartFilterBarService) { + constructor(dsCoreService: DSCoreService, private _smartFilterBar?: SmartFilterBarService) { super(dsCoreService) - this.variables$ = this._smartFilterBar.onChange().pipe( + this.variables$ = this._smartFilterBar?.onChange().pipe( map((slicers) => slicers.filter((slicer) => !!slicer.dimension?.parameter)), distinctUntilChanged(isEqual) - ) + ) ?? of([]) } override onInit() { From 3d8a90d3f120f52506e2d4105018ef25044dd73b Mon Sep 17 00:00:00 2001 From: meta-d Date: Sat, 27 Jul 2024 00:26:04 +0800 Subject: [PATCH 33/53] feat: create chart widget --- .../app/features/story/copilot/page/graph.ts | 17 ++- .../src/app/features/story/copilot/tools.ts | 134 +++++++++++++++--- 2 files changed, 125 insertions(+), 26 deletions(-) diff --git a/apps/cloud/src/app/features/story/copilot/page/graph.ts b/apps/cloud/src/app/features/story/copilot/page/graph.ts index 69472aaab..4ffc372c9 100644 --- a/apps/cloud/src/app/features/story/copilot/page/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/page/graph.ts @@ -3,9 +3,15 @@ import { SystemMessage } from '@langchain/core/messages' import { SystemMessagePromptTemplate } from '@langchain/core/prompts' import { CreateGraphOptions, createReactAgent } from '@metad/copilot' import { NGXLogger } from 'ngx-logger' -import { pageAgentState } from './types' +import { + injectCreateChartTool, + injectCreateFilterBarTool, + injectCreateInputControlTool, + injectCreateKPITool, + injectCreateVariableTool +} from '../tools' import { injectCreatePageTools } from './tools' -import { injectCreateFilterBarTool, injectCreateKPITool, injectCreateVariableTool } from '../tools' +import { pageAgentState } from './types' export function injectCreatePageAgent() { const logger = inject(NGXLogger) @@ -13,15 +19,16 @@ export function injectCreatePageAgent() { const createFilterBar = injectCreateFilterBarTool() const createKPI = injectCreateKPITool() const createVariable = injectCreateVariableTool() + const createInputControl = injectCreateInputControlTool() + const createChart = injectCreateChartTool() return async ({ llm, interruptBefore, interruptAfter }: CreateGraphOptions) => { - return createReactAgent({ state: pageAgentState, llm, interruptBefore, interruptAfter, - tools: [...tools, createFilterBar, createKPI, createVariable], + tools: [...tools, createFilterBar, createKPI, createVariable, createInputControl, createChart], messageModifier: async (state) => { const systemTemplate = `You are a BI analysis expert. {{role}} @@ -33,7 +40,7 @@ Step 2. 根据提供的 Cube context 和分析主题逐个向 dashboard 中添 Widget 类型分为 FilterBar, InputControl, Table, Chart, and KPI。 - 页面 layout 布局默认是 40 * 40. -- If there are variables in the cube, be sure to call 'createVariableControl' to create an input control widget for each variable to control the input value. +- When creating a FilterBar widget, if there are variables in the cube, please add the variables (Use variable name as dimension) to the Filterbar dimensions first. The cube context: {{context}} diff --git a/apps/cloud/src/app/features/story/copilot/tools.ts b/apps/cloud/src/app/features/story/copilot/tools.ts index d9e95cb09..e060aca4d 100644 --- a/apps/cloud/src/app/features/story/copilot/tools.ts +++ b/apps/cloud/src/app/features/story/copilot/tools.ts @@ -1,11 +1,25 @@ import { inject } from '@angular/core' import { MatDialog } from '@angular/material/dialog' import { DynamicStructuredTool } from '@langchain/core/tools' -import { DataSettingsSchema, DimensionSchema, markdownEntityType, MeasureSchema, VariableSchema } from '@metad/core' +import { + DataSettingsSchema, + DimensionMemberSchema, + markdownEntityType, + MeasureSchema, + SlicerSchema, + tryFixSlicer, + VariableSchema +} from '@metad/core' import { NgmDSCoreService } from '@metad/ocap-angular/core' -import { omit } from '@metad/ocap-core' +import { DataSettings, omit } from '@metad/ocap-core' import { FilterControlType, NxStoryService, WidgetComponentType } from '@metad/story/core' -import { createWidgetSchema } from '@metad/story/story' +import { + chartAnnotationCheck, + ChartSchema, + ChartWidgetSchema, + completeChartAnnotation, + createWidgetSchema +} from '@metad/story/story' import { NGXLogger } from 'ngx-logger' import { firstValueFrom } from 'rxjs' import z from 'zod' @@ -40,7 +54,6 @@ export function injectPickCubeTool() { return pickCubeTool } - export function injectCreateFilterBarTool() { const logger = inject(NGXLogger) const storyService = inject(NxStoryService) @@ -51,16 +64,11 @@ export function injectCreateFilterBarTool() { schema: z.object({ dataSettings: DataSettingsSchema, widget: createWidgetSchema({ - dimensions: z.array(DimensionSchema), + dimensions: z.array(DimensionMemberSchema) }) }), func: async ({ dataSettings, widget }) => { - logger.debug( - `Execute copilot action 'createFilterBar' using dataSettings:`, - dataSettings, - `widget:`, - widget - ) + logger.debug(`Execute copilot action 'createFilterBar' using dataSettings:`, dataSettings, `widget:`, widget) try { storyService.createStoryWidget({ ...omit(widget, 'dimensions'), @@ -81,7 +89,6 @@ export function injectCreateFilterBarTool() { }) } - export function injectCreateKPITool() { const logger = inject(NGXLogger) const storyService = inject(NxStoryService) @@ -97,12 +104,7 @@ export function injectCreateKPITool() { }) }), func: async ({ dataSettings, widget }) => { - logger.debug( - `Execute copilot action 'createKPI' using dataSettings:`, - dataSettings, - `widget:`, - widget - ) + logger.debug(`Execute copilot action 'createKPI' using dataSettings:`, dataSettings, `widget:`, widget) try { storyService.createStoryWidget({ ...omit(widget, 'kpiValue', 'kpiTarget'), @@ -126,7 +128,6 @@ export function injectCreateKPITool() { }) } - export function injectCreateVariableTool() { const logger = inject(NGXLogger) const storyService = inject(NxStoryService) @@ -137,7 +138,7 @@ export function injectCreateVariableTool() { schema: z.object({ dataSettings: DataSettingsSchema, widget: createWidgetSchema({ - variable: VariableSchema.describe('variable'), + variable: VariableSchema.describe('variable') }) }), func: async ({ dataSettings, widget }) => { @@ -169,4 +170,95 @@ export function injectCreateVariableTool() { return `The new input control widget has been created!` } }) -} \ No newline at end of file +} + +export function injectCreateInputControlTool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + return new DynamicStructuredTool({ + name: 'createInputControl', + description: 'Create a input control widget for dimension', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema({ + dimension: DimensionMemberSchema + }) + }), + func: async ({ dataSettings, widget }) => { + logger.debug(`Execute copilot action 'createInputControl' using dataSettings:`, dataSettings, `widget:`, widget) + + try { + storyService.createStoryWidget({ + ...omit(widget, 'dimension'), + dataSettings: { + ...dataSettings, + dimension: widget.dimension + }, + options: {}, + component: WidgetComponentType.InputControl + }) + } catch (error: any) { + return `Error while creating the input control: ${error.message}` + } + + return `The new input control widget has been created!` + } + }) +} + +export function injectCreateChartTool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + const createChartTool = new DynamicStructuredTool({ + name: 'createChartWidget', + description: 'Create a new widget in story page.', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema({ + chart: ChartSchema.describe('Chart configuration'), + slicers: z.array( + SlicerSchema + ) + .optional() + .describe('The slicers used by the chart data') + }), + }), + func: async ({ dataSettings, widget }) => { + logger.debug( + '[Story] [AI Copilot] [Command tool] [createChartWidget] inputs:', + 'dataSettings:', + dataSettings, + 'position:', + widget, + 'widget:', + ) + + const entityType = await firstValueFrom(storyService.selectEntityType(dataSettings as DataSettings)) + + try { + storyService.createStoryWidget({ + component: WidgetComponentType.AnalyticalCard, + position: widget.position ?? { x: 0, y: 0, rows: 5, cols: 5 }, + title: widget.title, + dataSettings: { + ...(dataSettings ?? {}), + chartAnnotation: completeChartAnnotation(chartAnnotationCheck(widget.chart, entityType)), + selectionVariant: { + selectOptions: (widget.slicers ?? ((widget.chart).slicers as any[]))?.map((slicer) => + tryFixSlicer(slicer, entityType) + ) + } + } + }) + } catch (error) { + return `Error: ${error}` + } + + return `The new chart widget has been created!` + } + }) + + return createChartTool +} From b89f6f96c4b353ad082d03d22297cbb2899a63f5 Mon Sep 17 00:00:00 2001 From: meta-d Date: Mon, 29 Jul 2024 19:51:28 +0800 Subject: [PATCH 34/53] feat: story page agent --- .../features/story/copilot/page/command.ts | 4 +- .../app/features/story/copilot/page/graph.ts | 35 +- .../app/features/story/copilot/page/index.ts | 3 +- .../features/story/copilot/story/command.ts | 13 +- .../app/features/story/copilot/story/graph.ts | 81 ++--- .../app/features/story/copilot/story/types.ts | 2 + .../src/app/features/story/copilot/tools.ts | 49 ++- .../src/app/features/story/designer/index.ts | 7 +- .../story-designer.component.html | 29 +- .../story-designer.component.scss | 2 +- .../story-designer.component.ts | 49 ++- .../story/designer/text/text.component.html | 71 ++++ .../story/designer/text/text.component.scss | 5 + .../story/designer/text/text.component.ts | 164 +++++++++ .../designer/widget/widget.component.html | 74 +--- .../story/designer/widget/widget.component.ts | 99 ++---- .../story/toolbar/toolbar.component.html | 29 +- .../story/toolbar/toolbar.component.ts | 150 ++++---- apps/cloud/src/app/widgets.ts | 2 +- apps/cloud/src/assets/i18n/zh-Hans.json | 1 + .../src/lib/copilot/cube.schema.ts | 14 +- libs/story-angular/core/types.ts | 14 + libs/story-angular/i18n/zhHans.ts | 1 + .../src/lib/settings/preferences/schema.ts | 89 +++-- .../story/copilot/schema/chart.schema.ts | 51 +-- .../story/copilot/schema/grid.schema.ts | 2 +- .../story-widget/story-widget.component.ts | 320 +++++++++++------- .../analytical-card.component.ts | 46 ++- .../widgets/kpi/kpi.component.ts | 14 +- .../widgets/kpi/kpi.styling.schema.ts | 4 +- .../widgets/swiper/swiper.component.html | 1 - .../widgets/tab-group/tabset.component.html | 1 - .../analytical-card.component.ts | 3 +- .../src/lib/chat/chat.component.html | 2 +- packages/copilot/src/lib/graph/team.ts | 2 +- 35 files changed, 884 insertions(+), 549 deletions(-) create mode 100644 apps/cloud/src/app/features/story/designer/text/text.component.html create mode 100644 apps/cloud/src/app/features/story/designer/text/text.component.scss create mode 100644 apps/cloud/src/app/features/story/designer/text/text.component.ts diff --git a/apps/cloud/src/app/features/story/copilot/page/command.ts b/apps/cloud/src/app/features/story/copilot/page/command.ts index 55807462b..9c303f986 100644 --- a/apps/cloud/src/app/features/story/copilot/page/command.ts +++ b/apps/cloud/src/app/features/story/copilot/page/command.ts @@ -5,7 +5,7 @@ import { NxStoryService } from '@metad/story/core' import { TranslateService } from '@ngx-translate/core' import { injectAgentFewShotTemplate, injectExampleRetriever } from 'apps/cloud/src/app/@core/copilot' import { NGXLogger } from 'ngx-logger' -import { injectCreatePageAgent } from './graph' +import { injectCreatePageGraph } from './graph' import { STORY_PAGE_COMMAND_NAME } from './types' export function injectStoryPageCommand() { @@ -13,7 +13,7 @@ export function injectStoryPageCommand() { const translate = inject(TranslateService) const storyService = inject(NxStoryService) - const createGraph = injectCreatePageAgent() + const createGraph = injectCreatePageGraph() const examplesRetriever = injectExampleRetriever(STORY_PAGE_COMMAND_NAME, { k: 5, vectorStore: null }) const fewShotPrompt = injectAgentFewShotTemplate(STORY_PAGE_COMMAND_NAME, { k: 1, vectorStore: null }) diff --git a/apps/cloud/src/app/features/story/copilot/page/graph.ts b/apps/cloud/src/app/features/story/copilot/page/graph.ts index 4ffc372c9..abcf4b611 100644 --- a/apps/cloud/src/app/features/story/copilot/page/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/page/graph.ts @@ -1,19 +1,22 @@ import { inject } from '@angular/core' -import { SystemMessage } from '@langchain/core/messages' +import { HumanMessage, SystemMessage } from '@langchain/core/messages' import { SystemMessagePromptTemplate } from '@langchain/core/prompts' +import { RunnableLambda } from '@langchain/core/runnables' import { CreateGraphOptions, createReactAgent } from '@metad/copilot' +import { injectAgentFewShotTemplate } from 'apps/cloud/src/app/@core/copilot' import { NGXLogger } from 'ngx-logger' import { injectCreateChartTool, injectCreateFilterBarTool, injectCreateInputControlTool, injectCreateKPITool, + injectCreateTableTool, injectCreateVariableTool } from '../tools' import { injectCreatePageTools } from './tools' -import { pageAgentState } from './types' +import { PageAgentState, pageAgentState, STORY_PAGE_COMMAND_NAME } from './types' -export function injectCreatePageAgent() { +export function injectCreatePageGraph() { const logger = inject(NGXLogger) const tools = injectCreatePageTools() const createFilterBar = injectCreateFilterBarTool() @@ -21,6 +24,7 @@ export function injectCreatePageAgent() { const createVariable = injectCreateVariableTool() const createInputControl = injectCreateInputControlTool() const createChart = injectCreateChartTool() + const createTable = injectCreateTableTool() return async ({ llm, interruptBefore, interruptAfter }: CreateGraphOptions) => { return createReactAgent({ @@ -28,7 +32,7 @@ export function injectCreatePageAgent() { llm, interruptBefore, interruptAfter, - tools: [...tools, createFilterBar, createKPI, createVariable, createInputControl, createChart], + tools: [...tools, createFilterBar, createKPI, createVariable, createInputControl, createChart, createTable], messageModifier: async (state) => { const systemTemplate = `You are a BI analysis expert. {{role}} @@ -53,3 +57,26 @@ The cube context: }) } } + +export function injectCreatePageAgent() { + const createPageGraph = injectCreatePageGraph() + const fewShotPrompt = injectAgentFewShotTemplate(STORY_PAGE_COMMAND_NAME, { k: 1, vectorStore: null }) + + return async ({ llm }: CreateGraphOptions) => { + const agent = await createPageGraph({ llm }) + + return RunnableLambda.from(async (state: PageAgentState) => { + const content = await fewShotPrompt.format({ input: state.input }) + + const { messages } = await agent.invoke({ + input: state.input, + messages: [new HumanMessage(content)], + role: state.role, + language: state.language, + context: state.context + }) + + return messages + }) + } +} diff --git a/apps/cloud/src/app/features/story/copilot/page/index.ts b/apps/cloud/src/app/features/story/copilot/page/index.ts index efb90b590..6e59c8b07 100644 --- a/apps/cloud/src/app/features/story/copilot/page/index.ts +++ b/apps/cloud/src/app/features/story/copilot/page/index.ts @@ -1 +1,2 @@ -export * from './command' \ No newline at end of file +export * from './command' +export * from './graph' \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/copilot/story/command.ts b/apps/cloud/src/app/features/story/copilot/story/command.ts index 0cc022a98..102c26a75 100644 --- a/apps/cloud/src/app/features/story/copilot/story/command.ts +++ b/apps/cloud/src/app/features/story/copilot/story/command.ts @@ -4,6 +4,8 @@ import { injectCopilotCommand } from '@metad/copilot-angular' import { TranslateService } from '@ngx-translate/core' import { NGXLogger } from 'ngx-logger' import { injectCreateStoryGraph } from './graph' +import { injectExampleRetriever } from 'apps/cloud/src/app/@core/copilot' +import { STORY_COMMAND_NAME } from './types' export function injectStoryCommand() { const logger = inject(NGXLogger) @@ -11,8 +13,8 @@ export function injectStoryCommand() { const createGraph = injectCreateStoryGraph() - const commandName = 'story' - return injectCopilotCommand(commandName, { + const examplesRetriever = injectExampleRetriever(STORY_COMMAND_NAME, { k: 5, vectorStore: null }) + return injectCopilotCommand(STORY_COMMAND_NAME, { alias: 's', description: translate.instant('PAC.Story.CommandStoryDesc', { Default: 'Describe the story you want' @@ -21,9 +23,12 @@ export function injectStoryCommand() { type: CopilotAgentType.Graph, conversation: true, interruptBefore: [ - 'calculation' - ] + 'calculation', + 'page', + 'widget' + ], }, + examplesRetriever, createGraph }) } diff --git a/apps/cloud/src/app/features/story/copilot/story/graph.ts b/apps/cloud/src/app/features/story/copilot/story/graph.ts index 4b874dc2c..088b74d75 100644 --- a/apps/cloud/src/app/features/story/copilot/story/graph.ts +++ b/apps/cloud/src/app/features/story/copilot/story/graph.ts @@ -1,13 +1,14 @@ import { inject, signal } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { HumanMessage, isAIMessage, ToolMessage } from '@langchain/core/messages' +import { HumanMessage } from '@langchain/core/messages' import { RunnableLambda } from '@langchain/core/runnables' -import { END, START, StateGraph } from '@langchain/langgraph/web' -import { AgentState, CreateGraphOptions, Team } from '@metad/copilot' +import { START, StateGraph } from '@langchain/langgraph/web' +import { CreateGraphOptions, Team } from '@metad/copilot' import { DataSettings } from '@metad/ocap-core' import { injectCreateWidgetAgent } from '@metad/story/story' import { NGXLogger } from 'ngx-logger' -import { CalculationAgentState, injectCreateCalculationGraph } from '../calculation' +import { injectCreateCalculationGraph } from '../calculation' +import { injectCreatePageAgent } from '../page' import { StoryAgentState, storyAgentState } from './types' export function injectCreateStoryGraph() { @@ -27,24 +28,12 @@ export function injectCreateStoryGraph() { } ) + const createPageAgent = injectCreatePageAgent() const createWidgetGraph = injectCreateWidgetAgent() return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => { const calculationAgent = (await createCalculationGraph({ llm })).compile() - - const shouldContinue = (state: AgentState) => { - const { messages } = state - const lastMessage = messages[messages.length - 1] - if (isAIMessage(lastMessage)) { - if (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0) { - return END - } else { - return lastMessage.tool_calls[0].args.next - } - } else { - return END - } - } + const pageAgent = await createPageAgent({ llm }) const superAgent = await Team.createSupervisorAgent( llm, @@ -53,6 +42,10 @@ export function injectCreateStoryGraph() { name: 'calcualtion', description: 'create a calculation measure for cube' }, + { + name: 'page', + description: '' + }, { name: 'widget', description: 'create a widget in story dashboard' @@ -65,6 +58,7 @@ export function injectCreateStoryGraph() { {context} Story dashbaord 通常由多个页面组成,每个页面是一个分析主题,每个主题的页面通常由一个过滤器栏、多个主要的维度输入控制器、多个指标、多个图形、一个或多个表格组成。 +- 过滤器栏通常包含 3 至 8 个重要的维度过滤器,不要太多。 `, `If you need to execute a task, you need to get confirmation before calling the route function.` ) @@ -80,27 +74,29 @@ Story dashbaord 通常由多个页面组成,每个页面是一个分析主题 .addNode( 'calculation', RunnableLambda.from(async (state: StoryAgentState) => { - // const content = await fewShotPrompt.format({ input: state.input, context: state.context }) - return { - input: state.input, + const { messages } = await calculationAgent.invoke({ + input: state.instructions, messages: [new HumanMessage(state.instructions)], role: state.role, context: state.context, - tool_call_id: state.tool_call_id - } + language: state.language + }) + return Team.responseToolMessage(state.tool_call_id, messages) }) - .pipe(calculationAgent) - .pipe((response: CalculationAgentState) => { - return { - tool_call_id: null, - messages: [ - new ToolMessage({ - tool_call_id: response.tool_call_id, - content: response.messages[response.messages.length - 1].content - }) - ] - } + ) + .addNode( + 'page', + RunnableLambda.from(async (state: StoryAgentState) => { + const messages = await pageAgent.invoke({ + input: state.instructions, + role: state.role, + context: state.context, + language: state.language, + messages: [] }) + + return Team.responseToolMessage(state.tool_call_id, messages) + }) ) .addNode( 'widget', @@ -112,24 +108,13 @@ Story dashbaord 通常由多个页面组成,每个页面是一个分析主题 context: state.context }) - return { - tool_call_id: null, - messages: [ - new ToolMessage({ - tool_call_id: state.tool_call_id, - content: messages[messages.length - 1].content - }) - ] - } + return Team.responseToolMessage(state.tool_call_id, messages) }) ) .addEdge('calculation', Team.SUPERVISOR_NAME) + .addEdge('page', Team.SUPERVISOR_NAME) .addEdge('widget', Team.SUPERVISOR_NAME) - .addConditionalEdges(Team.SUPERVISOR_NAME, shouldContinue, { - calculation: 'calculation', - widget: 'widget', - [END]: END - }) + .addConditionalEdges(Team.SUPERVISOR_NAME, Team.supervisorRouter) .addEdge(START, Team.SUPERVISOR_NAME) return superGraph diff --git a/apps/cloud/src/app/features/story/copilot/story/types.ts b/apps/cloud/src/app/features/story/copilot/story/types.ts index 0573cf69c..2e22f888a 100644 --- a/apps/cloud/src/app/features/story/copilot/story/types.ts +++ b/apps/cloud/src/app/features/story/copilot/story/types.ts @@ -1,6 +1,8 @@ import { StateGraphArgs } from '@langchain/langgraph/web' import { Team } from '@metad/copilot' +export const STORY_COMMAND_NAME = 'story' + export interface StoryAgentState extends Team.State { tool_call_id: string } diff --git a/apps/cloud/src/app/features/story/copilot/tools.ts b/apps/cloud/src/app/features/story/copilot/tools.ts index e060aca4d..d78e2f534 100644 --- a/apps/cloud/src/app/features/story/copilot/tools.ts +++ b/apps/cloud/src/app/features/story/copilot/tools.ts @@ -1,6 +1,7 @@ import { inject } from '@angular/core' import { MatDialog } from '@angular/material/dialog' import { DynamicStructuredTool } from '@langchain/core/tools' +import { nanoid } from '@metad/copilot' import { DataSettingsSchema, DimensionMemberSchema, @@ -16,8 +17,9 @@ import { FilterControlType, NxStoryService, WidgetComponentType } from '@metad/s import { chartAnnotationCheck, ChartSchema, - ChartWidgetSchema, + tryFixAnalyticsAnnotation, completeChartAnnotation, + createTableWidgetSchema, createWidgetSchema } from '@metad/story/story' import { NGXLogger } from 'ngx-logger' @@ -227,7 +229,7 @@ export function injectCreateChartTool() { }), func: async ({ dataSettings, widget }) => { logger.debug( - '[Story] [AI Copilot] [Command tool] [createChartWidget] inputs:', + '[Story] [AI Copilot] [Command tool] [createChart] inputs:', 'dataSettings:', dataSettings, 'position:', @@ -262,3 +264,46 @@ export function injectCreateChartTool() { return createChartTool } + + +export function injectCreateTableTool() { + const logger = inject(NGXLogger) + const storyService = inject(NxStoryService) + + const createTableTool = new DynamicStructuredTool({ + name: 'createTableWidget', + description: 'Create a new table widget.', + schema: z.object({ + dataSettings: DataSettingsSchema, + widget: createWidgetSchema(createTableWidgetSchema()) + }), + func: async ({dataSettings, widget}) => { + logger.debug( + '[Story] [AI Copilot] [Command tool] [createTableWidget] inputs:', + dataSettings, + widget, + ) + + const entityType = await firstValueFrom(storyService.selectEntityType(dataSettings as DataSettings)) + + try { + const key = nanoid() + storyService.createStoryWidget({ + key, + ...widget, + component: WidgetComponentType.AnalyticalGrid, + dataSettings: { + ...(dataSettings ?? {}), + analytics: tryFixAnalyticsAnnotation(widget.analytics, entityType) + }, + }) + + return `Story table widget '${key}' created!` + } catch (error: any) { + return `Error: ${error.message}` + } + } + }) + + return createTableTool +} diff --git a/apps/cloud/src/app/features/story/designer/index.ts b/apps/cloud/src/app/features/story/designer/index.ts index 0c551424e..d0ede30f4 100644 --- a/apps/cloud/src/app/features/story/designer/index.ts +++ b/apps/cloud/src/app/features/story/designer/index.ts @@ -1,6 +1,7 @@ import { FORMLY_CONFIG } from '@ngx-formly/core' import { PACFormlyImageUploadComponent } from './image-upload/image-upload.component' import { PACFormlyWidgetDesignerComponent } from './widget/widget.component' +import { PACFormlyTextDesignerComponent } from './text/text.component' export * from './image-upload/image-upload.component' export * from './page-designer/page-designer.component' @@ -18,10 +19,14 @@ export function provideFormlyStory() { name: 'styling', component: PACFormlyWidgetDesignerComponent }, + { + name: 'text-css', + component: PACFormlyTextDesignerComponent + }, { name: 'image-upload', component: PACFormlyImageUploadComponent - } + }, ] } } diff --git a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.html b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.html index 074f90403..2d97a790d 100644 --- a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.html +++ b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.html @@ -1,10 +1,19 @@ -
- {{ 'PAC.Story.GlobalStyles' | translate: {Default: 'Global Styles'} }} -
- \ No newline at end of file + + + + + + + + diff --git a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.scss b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.scss index 67a93feeb..5e6888f1f 100644 --- a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.scss +++ b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.scss @@ -1,3 +1,3 @@ :host { - @apply flex flex-col overflow-auto; + @apply flex flex-col overflow-hidden; } diff --git a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.ts b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.ts index 06b2144bd..2541b86d4 100644 --- a/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.ts +++ b/apps/cloud/src/app/features/story/designer/story-designer/story-designer.component.ts @@ -1,16 +1,17 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop' import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms' import { AppearanceDirective, DensityDirective } from '@metad/ocap-angular/core' import { assign, cloneDeep } from '@metad/ocap-core' +import { PreferencesSchema, StoryPreferencesFields } from '@metad/story' +import { NxStoryService, StoryPreferences } from '@metad/story/core' +import { FORMLY_W_1_2 } from '@metad/story/designer' import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core' import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { PreferencesSchema } from '@metad/story' -import { NxStoryService } from '@metad/story/core' -import { debounceTime } from 'rxjs' +import { combineLatest, debounceTime, map, startWith } from 'rxjs' import { InlineSearchComponent, MaterialModule } from '../../../../@shared' import { DesignerWidgetComponent } from '../widget/widget.component' -import { takeUntilDestroyed } from '@angular/core/rxjs-interop' @Component({ standalone: true, @@ -46,21 +47,47 @@ export class StoryDesignerComponent { options: FormlyFormOptions model = {} - private formGroupSub = this.formGroup.valueChanges.pipe(debounceTime(300), takeUntilDestroyed()).subscribe((value) => { - this.storyService.updateStoryPreferences(value) - }) + // For story + readonly storyFields = toSignal( + this.translateService.get('Story').pipe( + map((i18n) => { + return StoryPreferencesFields(FORMLY_W_1_2, i18n) + }) + ) + ) + storyFormGroup = new FormGroup({}) + storyOptions: FormlyFormOptions + storyModel = {} - private preferencesSub = this.translateService.get('Story').pipe(takeUntilDestroyed()).subscribe((CSS) => { - this.fields = PreferencesSchema(CSS) - }) + private valueSub = combineLatest([ + this.formGroup.valueChanges.pipe(startWith(null)), + this.storyFormGroup.valueChanges.pipe(startWith(null)) + ]) + .pipe(debounceTime(300), takeUntilDestroyed()) + .subscribe(([preferences, story]) => { + const value = { ...(preferences ?? {}) } as StoryPreferences + if (story) { + value.story = story + } + this.storyService.updateStoryPreferences(value) + }) + + private preferencesSub = this.translateService + .get('Story') + .pipe(takeUntilDestroyed()) + .subscribe((CSS) => { + this.fields = PreferencesSchema(CSS) + }) async ngOnInit() { const preferences = this.storyService.preferences() ?? {} this.formGroup.patchValue(preferences) this.model = assign(this.model, cloneDeep(preferences)) + this.storyFormGroup.patchValue(preferences.story ?? {}) + this.storyModel = assign(this.storyModel, cloneDeep(preferences.story ?? {})) } onModelChange(event) { - // console.log(event) + // } } diff --git a/apps/cloud/src/app/features/story/designer/text/text.component.html b/apps/cloud/src/app/features/story/designer/text/text.component.html new file mode 100644 index 000000000..4b995ad41 --- /dev/null +++ b/apps/cloud/src/app/features/story/designer/text/text.component.html @@ -0,0 +1,71 @@ + + + + + + + + +
+ + +
+ + + format_align_left + + + format_align_center + + + format_align_right + + + format_align_justify + + +
+ +
+ +
+ + +
+ + -- + @for (fWeight of fontWeights; track fWeight) { + {{fWeight}} + } + +
+
+ + + + + \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/designer/text/text.component.scss b/apps/cloud/src/app/features/story/designer/text/text.component.scss new file mode 100644 index 000000000..76ad4a639 --- /dev/null +++ b/apps/cloud/src/app/features/story/designer/text/text.component.scss @@ -0,0 +1,5 @@ +.mat-mdc-select { + --mat-select-trigger-text-size: 10px; + --mat-select-trigger-text-line-height: 12px; + padding: 0 10px; +} \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/designer/text/text.component.ts b/apps/cloud/src/app/features/story/designer/text/text.component.ts new file mode 100644 index 000000000..62ed01dcc --- /dev/null +++ b/apps/cloud/src/app/features/story/designer/text/text.component.ts @@ -0,0 +1,164 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core' +import { ControlValueAccessor, FormBuilder, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms' +import { NgmInputComponent, NgmSliderInputComponent } from '@metad/ocap-angular/common' +import { AppearanceDirective, DensityDirective } from '@metad/ocap-angular/core' +import { ComponentStyling } from '@metad/story/core' +import { FieldType, FormlyModule } from '@ngx-formly/core' +import { TranslateModule } from '@ngx-translate/core' +import { ColorInputComponent } from '../color-input/color-input.component' +import { MaterialModule } from '../../../../@shared' + +@Component({ + standalone: true, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule, + FormlyModule, + MaterialModule, + + AppearanceDirective, + DensityDirective, + NgmSliderInputComponent, + NgmInputComponent, + ColorInputComponent + ], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'pac-designer-text', + templateUrl: './text.component.html', + styleUrls: ['./text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => DesignerTextComponent) + } + ], + animations: [] +}) +export class DesignerTextComponent implements ControlValueAccessor { + private readonly formBuilder = inject(FormBuilder) + + + fontFamilies = [ + "Lato, 'Noto Serif SC', monospace", + "Arial, Helvetica, sans-serif", + "'Times New Roman', Times, serif", + "Verdana, Geneva, sans-serif", + "Georgia, serif", + "'Courier New', Courier, monospace", + "Tahoma, Geneva, sans-serif", + "'Trebuchet MS', sans-serif", + "Palatino, serif", + "Impact, Charcoal, sans-serif", + "'Lucida Sans Unicode', 'Lucida Grande', sans-serif" + ] + fontFamilyOptions = [ + { + value: null, + label: '--' + }, + ...this.fontFamilies.map((value) => ({value})) + ] + fontWeights = [ + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ] + + formGroup = this.formBuilder.group({ + color: null, + fontSize: null, + lineHeight: null, + textAlign: null, + fontFamily: null, + fontWeight: null, + textShadow: null, + filter: null, + opacity: null + } as any) + + + get color() { + return this.formGroup.get('color') as FormControl + } + get fontSize() { + return this.formGroup.get('fontSize') as FormControl + } + get lineHeight() { + return this.formGroup.get('lineHeight') as FormControl + } + get fontWeight() { + return this.formGroup.get('fontWeight') as FormControl + } + get fontFamily() { + return this.formGroup.get('fontFamily') as FormControl + } + get textAlign() { + return this.formGroup.get('textAlign')!.value + } + set textAlign(value) { + this.formGroup.get('textAlign')!.setValue(value) + } + + get textShadow() { + return this.formGroup.get('textShadow') as FormControl + } + + get opacity() { + return this.formGroup.get('opacity') as FormControl + } + + private _onChange: (value: any) => void + + private valueSub = this.formGroup.valueChanges.subscribe((value) => { + if (this._onChange) { + this._onChange(value) + } + }) + + writeValue(obj: any): void { + if (obj) { + this.formGroup.patchValue(obj) + } + } + registerOnChange(fn: any): void { + this._onChange = fn + } + registerOnTouched(fn: any): void {} + setDisabledState?(isDisabled: boolean): void {} +} + + +@Component({ + standalone: true, + imports: [CommonModule, FormlyModule, TranslateModule, ReactiveFormsModule, DesignerTextComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'pac-formly-text-designer', + template: ` + +`, + styles: [ + ` + :host { + flex: 1; + max-width: 100%; + } + ` + ] +}) +export class PACFormlyTextDesignerComponent extends FieldType { + +} diff --git a/apps/cloud/src/app/features/story/designer/widget/widget.component.html b/apps/cloud/src/app/features/story/designer/widget/widget.component.html index ddc1eefc5..b98c52bce 100644 --- a/apps/cloud/src/app/features/story/designer/widget/widget.component.html +++ b/apps/cloud/src/app/features/story/designer/widget/widget.component.html @@ -315,76 +315,6 @@ [formControl]="shadowSpread" [min]="-200" [max]="200">
- - - - - - - - -
- - -
- - - format_align_left - - - format_align_center - - - format_align_right - - - format_align_justify - - -
- -
- -
- - -
- - -- - {{fWeight}} - -
-
- - - - - -
+ + \ No newline at end of file diff --git a/apps/cloud/src/app/features/story/designer/widget/widget.component.ts b/apps/cloud/src/app/features/story/designer/widget/widget.component.ts index f49e52079..a49b8d78f 100644 --- a/apps/cloud/src/app/features/story/designer/widget/widget.component.ts +++ b/apps/cloud/src/app/features/story/designer/widget/widget.component.ts @@ -22,6 +22,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard' import { sortBy } from 'lodash-es' import { NgmInputComponent, NgmSliderInputComponent } from '@metad/ocap-angular/common' import { BackdropFilterEnum, FilterEnum } from '@metad/core' +import { DesignerTextComponent } from '../text/text.component' @Component({ @@ -42,7 +43,8 @@ import { BackdropFilterEnum, FilterEnum } from '@metad/core' NgmSliderInputComponent, ColorInputComponent, ImageUploadComponent, - NgmInputComponent + NgmInputComponent, + DesignerTextComponent ], changeDetection: ChangeDetectionStrategy.OnPush, selector: 'pac-designer-widget', @@ -342,43 +344,6 @@ export class DesignerWidgetComponent implements ControlValueAccessor { label: key })) - fontFamilies = [ - "Lato, 'Noto Serif SC', monospace", - "Arial, Helvetica, sans-serif", - "'Times New Roman', Times, serif", - "Verdana, Geneva, sans-serif", - "Georgia, serif", - "'Courier New', Courier, monospace", - "Tahoma, Geneva, sans-serif", - "'Trebuchet MS', sans-serif", - "Palatino, serif", - "Impact, Charcoal, sans-serif", - "'Lucida Sans Unicode', 'Lucida Grande', sans-serif" - ] - fontFamilyOptions = [ - { - value: null, - label: '--' - }, - ...this.fontFamilies.map((value) => ({value})) - ] - - fontWeights = [ - 'normal', - 'bold', - 'bolder', - 'lighter', - '100', - '200', - '300', - '400', - '500', - '600', - '700', - '800', - '900' - ] - formGroup = this.formBuilder.group({ padding: null, borderRadius: null, @@ -411,6 +376,9 @@ export class DesignerWidgetComponent implements ControlValueAccessor { filter: null, opacity: null, }) + + readonly textFormControl = new FormControl({}) + get padding() { return this.formGroup.get('padding').value } @@ -511,31 +479,31 @@ export class DesignerWidgetComponent implements ControlValueAccessor { return this.borderColor.value } - get color() { - return this.formGroup.get('color') as FormControl - } - get fontSize() { - return this.formGroup.get('fontSize') as FormControl - } - get lineHeight() { - return this.formGroup.get('lineHeight') as FormControl - } - get fontWeight() { - return this.formGroup.get('fontWeight') as FormControl - } - get fontFamily() { - return this.formGroup.get('fontFamily') as FormControl - } - get textAlign() { - return this.formGroup.get('textAlign').value - } - set textAlign(value) { - this.formGroup.get('textAlign').setValue(value) - } - - get textShadow() { - return this.formGroup.get('textShadow') as FormControl - } + // get color() { + // return this.formGroup.get('color') as FormControl + // } + // get fontSize() { + // return this.formGroup.get('fontSize') as FormControl + // } + // get lineHeight() { + // return this.formGroup.get('lineHeight') as FormControl + // } + // get fontWeight() { + // return this.formGroup.get('fontWeight') as FormControl + // } + // get fontFamily() { + // return this.formGroup.get('fontFamily') as FormControl + // } + // get textAlign() { + // return this.formGroup.get('textAlign').value + // } + // set textAlign(value) { + // this.formGroup.get('textAlign').setValue(value) + // } + + // get textShadow() { + // return this.formGroup.get('textShadow') as FormControl + // } get transform() { return this.formGroup.get('transform') as FormControl } @@ -562,9 +530,14 @@ export class DesignerWidgetComponent implements ControlValueAccessor { this._onChange(value) }) + private textSub = this.textFormControl.valueChanges.subscribe((text) => { + this.formGroup.patchValue(text) + }) + writeValue(obj: any): void { if (obj) { this.formGroup.patchValue(obj) + this.textFormControl.patchValue(obj) } } registerOnChange(fn: any): void { diff --git a/apps/cloud/src/app/features/story/toolbar/toolbar.component.html b/apps/cloud/src/app/features/story/toolbar/toolbar.component.html index b17a03810..6984ae617 100644 --- a/apps/cloud/src/app/features/story/toolbar/toolbar.component.html +++ b/apps/cloud/src/app/features/story/toolbar/toolbar.component.html @@ -1,5 +1,6 @@
-
+ @if (expandLess() || !collapsible()) { +
@if (saving()) { @@ -121,24 +122,24 @@ keyboard_arrow_left - + auto_fix_high +
+ } @if (collapsible()) { } @@ -368,7 +369,9 @@
- +@if (showDetails() === 'storyDesigner') { + +}
@@ -448,12 +451,6 @@
- -