Skip to content

Commit 9678ea7

Browse files
authored
Merge pull request #51 from meta-d/develop
version 2.5.8
2 parents 64d2d33 + 7f08006 commit 9678ea7

File tree

52 files changed

+935
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+935
-328
lines changed

.deploy/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ocap-server",
33
"author": "Metad",
4-
"version": "2.5.7",
4+
"version": "2.5.8",
55
"scripts": {
66
"start": "nx serve",
77
"build": "nx build",

apps/cloud/src/app/@core/copilot/references-retriever.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function injectReferencesRetrieverTool(command: string | string[], option
1616
},
1717
{
1818
name: 'referencesRetriever',
19-
description: 'Retrieve references for a list of questions',
19+
description: `Retrieve references docs for a list of questions, such as: how to create a formula for calculated measure, how to create a time slicer for relative time`,
2020
schema: z.object({
2121
questions: z.array(z.string().describe('The question to retrieve references'))
2222
})

apps/cloud/src/app/@core/services/chatbi-conversation.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ export class ChatBIConversationService extends OrganizationBaseService {
3434
getMy() {
3535
return this.selectOrganizationId().pipe(
3636
switchMap(() =>
37-
this.httpClient.get<{ items: IChatBIConversation[] }>(API_CHATBI_CONVERSATION + '/my', {
37+
this.httpClient.get<{ items: IChatBIConversation[]; total: number; }>(API_CHATBI_CONVERSATION + '/my', {
3838
params: {
3939
data: JSON.stringify({
40+
take: 20,
41+
skip: 0,
4042
order: {
4143
createdAt: OrderTypeEnum.DESC
4244
}
4345
})
44-
}
46+
} as any
4547
})
4648
),
4749
map(({ items }) => items.map(convertChatBIConversationResult))

apps/cloud/src/app/features/chatbi/answer/answer.component.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</div>
1616
</div>
1717
<div class="flex-1 inline-flex flex-col items-start gap-y-3 overflow-hidden">
18-
<div class="answer-content flex-1 w-full flex flex-col items-start">
18+
<div class="answer-content flex-1 w-full flex flex-col items-stretch">
1919
<div class="w-full mb-1 font-semibold">
2020
{{'PAC.KEY_WORDS.Copilot' | translate: {Default: 'Copilot'} }}<span class="text-xs text-violet-500 bg-violet-500/10 px-1.5 py-1 rounded-full m-2">ChatBI</span>
2121
</div>
@@ -39,6 +39,14 @@
3939
/>
4040
}
4141
@case ('object') {
42+
@if (isQuestions(item); as q) {
43+
<h3 class="my-2">{{'PAC.ChatBI.TryFollowing' | translate: {Default: 'You can also try the following'} }}:</h3>
44+
<ul class="list-decimal pl-4">
45+
@for (question of q.questions; track question) {
46+
<li class="my-2 text-sm"><a class="cursor-pointer" (click)="edit(question)">{{question}}</a></li>
47+
}
48+
</ul>
49+
}
4250
@if (isAnswer(item); as answer) {
4351
@if (answer.indicators) {
4452
<div class="flex flex-col justify-start items-stretch gap-2">

apps/cloud/src/app/features/chatbi/answer/answer.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ExplainComponent } from '@metad/story/story'
3434
import { NxWidgetKpiComponent } from '@metad/story/widgets/kpi'
3535
import { WidgetService } from '@metad/core'
3636
import { ChatbiChatComponent } from '../chat/chat.component'
37+
import { ChatbiInputComponent } from '../input/input.component'
3738

3839
@Component({
3940
standalone: true,
@@ -89,6 +90,7 @@ export class ChatbiAnswerComponent {
8990
TOPS = [5, 10, 20, 100]
9091

9192
readonly message = input<CopilotChatMessage>(null)
93+
readonly chatInput = input<ChatbiInputComponent>(null)
9294
readonly analyticalCard = viewChild(AnalyticalCardComponent)
9395
readonly analyticalGrid = viewChild(AnalyticalGridComponent)
9496

@@ -183,6 +185,10 @@ export class ChatbiAnswerComponent {
183185
return value as unknown as QuestionAnswer
184186
}
185187

188+
isQuestions(value: string | any) {
189+
return typeof value === 'object' && Array.isArray(value['questions']) ? value as { questions: string[] } : null
190+
}
191+
186192
openExplore(item: string | QuestionAnswer) {
187193
this.homeComponent.openExplore(this.message(), item as unknown as QuestionAnswer)
188194
}
@@ -362,6 +368,10 @@ export class ChatbiAnswerComponent {
362368
}
363369
}
364370

371+
edit(text: string) {
372+
this.chatInput().prompt.set(text)
373+
}
374+
365375
@HostBinding('class.full-screen') get isFullscreen() {
366376
return this.fullscreen()
367377
}

apps/cloud/src/app/features/chatbi/chat/chat.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@
5252
@for (message of conversation()?.messages; track $index) {
5353
@switch (message.role) {
5454
@case ('assistant') {
55-
<pac-chatbi-answer class="group w-full p-4 md:p-6 mb-4 flex rounded-xl bg-gray-50 dark:bg-neutral-900" [message]="message" />
55+
<pac-chatbi-answer class="group w-full p-4 md:p-6 mb-4 flex rounded-xl bg-gray-50 dark:bg-neutral-900"
56+
[message]="message"
57+
[chatInput]="chatInput"
58+
/>
5659
}
5760
@case ('user') {
5861
<div class="w-full p-4 md:p-6 flex rounded-xl">
@@ -110,6 +113,6 @@
110113
</div>
111114
</div>
112115

113-
<pac-chatbi-input class="w-full py-2 md:pb-4 lg:px-2 flex flex-col sticky bottom-0 left-0"
116+
<pac-chatbi-input #chatInput class="w-full py-2 md:pb-4 lg:px-2 flex flex-col sticky bottom-0 left-0"
114117
[(prompt)]="prompt"
115118
/>

apps/cloud/src/app/features/chatbi/chat/chat.component.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { DensityDirective } from '@metad/ocap-angular/core'
2323
import { nonNullable } from '@metad/ocap-core'
2424
import { TranslateModule, TranslateService } from '@ngx-translate/core'
2525
import { MarkdownModule } from 'ngx-markdown'
26-
import { debounceTime, filter } from 'rxjs'
26+
import { debounceTime, distinctUntilChanged, filter } from 'rxjs'
2727
import { ChatbiAnswerComponent } from '../answer/answer.component'
2828
import { ChatbiService } from '../chatbi.service'
2929
import { injectExamplesAgent } from '../copilot'
@@ -64,20 +64,6 @@ export class ChatbiChatComponent {
6464

6565
readonly examples = this.chatbiService.examples
6666

67-
// readonly examples = toSignal(
68-
// this.translate
69-
// .stream('PAC.ChatBI.SystemMessage_Samples', {
70-
// Default: [
71-
// 'Monthly sales trends of Canadian customers in 2023',
72-
// 'Top 10 users in terms of spending in 2024',
73-
// 'What is the spending amount in each month of 2023',
74-
// 'Spending amount distribution of users in each channel in 2023',
75-
// 'How many users are there in each channel in 2023'
76-
// ]
77-
// })
78-
// .pipe(map((examples) => examples))
79-
// )
80-
8167
readonly cube = this.chatbiService.entity
8268
readonly entityType = this.chatbiService.entityType
8369

@@ -91,16 +77,14 @@ export class ChatbiChatComponent {
9177
.pipe(filter(nonNullable), debounceTime(1000), takeUntilDestroyed())
9278
.subscribe(() => this.scrollBottom())
9379

94-
constructor() {
95-
effect(
96-
() => {
97-
if (this.chatbiService.context() && this.examplesEmpty()) {
98-
this.refresh()
99-
}
100-
},
101-
{ allowSignalWrites: true }
102-
)
103-
}
80+
private examplesSub = toObservable(this.chatbiService.context).pipe(
81+
filter(nonNullable), debounceTime(1000), distinctUntilChanged(), takeUntilDestroyed()
82+
).subscribe(() => {
83+
if (this.examplesEmpty()) {
84+
this.refresh()
85+
}
86+
})
87+
10488

10589
editQuestion(message: CopilotChatMessage) {
10690
this.prompt.set(message.content)

apps/cloud/src/app/features/chatbi/chatbi.service.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CopilotChatMessage, nanoid } from '@metad/copilot'
55
import { markdownModelCube } from '@metad/core'
66
import { NgmDSCoreService } from '@metad/ocap-angular/core'
77
import { WasmAgentService } from '@metad/ocap-angular/wasm-agent'
8-
import { EntityType, Indicator, isEntityType, isEqual, isString, omitBlank, Schema } from '@metad/ocap-core'
8+
import { EntityType, Indicator, isEntityType, isEqual, isString, nonNullable, omit, omitBlank, Schema } from '@metad/ocap-core'
99
import { getSemanticModelKey } from '@metad/story/core'
1010
import { derivedAsync } from 'ngxtension/derived-async'
1111
import {
@@ -22,13 +22,15 @@ import {
2222
} from 'rxjs'
2323
import { ChatBIConversationService, ChatbiConverstion, registerModel } from '../../@core'
2424
import { QuestionAnswer } from './types'
25+
import { injectQueryParams } from 'ngxtension/inject-query-params'
2526

2627
@Injectable()
2728
export class ChatbiService {
2829
readonly #modelsService = inject(ModelsService)
2930
readonly #dsCoreService = inject(NgmDSCoreService)
3031
readonly #wasmAgent = inject(WasmAgentService)
3132
readonly conversationService = inject(ChatBIConversationService)
33+
readonly paramId = injectQueryParams('id')
3234

3335
readonly models$ = this.#modelsService.getMy()
3436
readonly detailModels = signal<Record<string, NgmSemanticModel>>({})
@@ -70,9 +72,8 @@ export class ChatbiService {
7072
readonly entityType = derivedAsync<EntityType>(() => {
7173
const dataSourceName = this.dataSourceName()
7274
const cube = this.entity()
73-
if (dataSourceName && cube) {
74-
return this.#dsCoreService.getDataSource(dataSourceName).pipe(
75-
switchMap((dataSource) => dataSource.selectEntityType(cube)),
75+
if (dataSourceName && this.dataSource() && cube) {
76+
return this.dataSource().selectEntityType(cube).pipe(
7677
filter((entityType) => isEntityType(entityType))
7778
) as Observable<EntityType>
7879
}
@@ -100,6 +101,7 @@ export class ChatbiService {
100101

101102
readonly pristineConversation = signal<ChatbiConverstion | null>(null)
102103
readonly indicators = computed(() => this.conversation()?.indicators ?? [])
104+
readonly modelIndicators = computed(() => this.model()?.indicators)
103105

104106
readonly aiMessage = signal<CopilotChatMessage>(null)
105107

@@ -108,10 +110,10 @@ export class ChatbiService {
108110
.pipe(takeUntilDestroyed())
109111
.subscribe((items) => {
110112
this.conversations.set(items)
111-
if (!this.conversationId()) {
112-
this.setConversation(items[0]?.key)
113-
}
114-
if (!this.conversationKey()) {
113+
// if (!this.conversationId()) {
114+
// this.setConversation(items[0]?.key)
115+
// }
116+
if (!this.paramId() && !this.conversationKey()) {
115117
this.newConversation()
116118
}
117119
})
@@ -143,6 +145,14 @@ export class ChatbiService {
143145
}
144146
})
145147

148+
private modelSub = toObservable(this.model).pipe(filter(nonNullable), takeUntilDestroyed())
149+
.subscribe((model) => {
150+
this.registerModel(model)
151+
if (!this.entity() && model.cube) {
152+
this.setEntity(model.cube)
153+
}
154+
})
155+
146156
constructor() {
147157
effect(
148158
async () => {
@@ -159,10 +169,10 @@ export class ChatbiService {
159169
)
160170
)
161171
this.detailModels.update((state) => ({ ...state, [model.id]: model }))
162-
this.registerModel(model)
163-
if (!this.entity() && model.cube) {
164-
this.setEntity(model.cube)
165-
}
172+
// this.registerModel(model)
173+
// if (!this.entity() && model.cube) {
174+
// this.setEntity(model.cube)
175+
// }
166176
}
167177
},
168178
{ allowSignalWrites: true }
@@ -183,8 +193,8 @@ export class ChatbiService {
183193
const dataSource = this.dataSource()
184194
const indicators = this.indicators()
185195
if (dataSource && indicators) {
186-
const schema = dataSource.options.schema
187-
const _indicators = [...(schema?.indicators ?? [])].filter(
196+
// const schema = dataSource.options.schema
197+
const _indicators = [...(this.modelIndicators() ?? [])].filter(
188198
(indicator) => !indicators.some((item) => item.id === indicator.id || item.code === indicator.code)
189199
)
190200
_indicators.push(...indicators)
@@ -210,7 +220,7 @@ export class ChatbiService {
210220
}
211221

212222
private registerModel(model: NgmSemanticModel) {
213-
registerModel(model, this.#dsCoreService, this.#wasmAgent)
223+
registerModel(omit(model, 'indicators'), this.#dsCoreService, this.#wasmAgent)
214224
}
215225

216226
newConversation() {

apps/cloud/src/app/features/chatbi/copilot/chatbi/graph.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inject } from '@angular/core'
2-
import { SystemMessage } from '@langchain/core/messages'
2+
import { isAIMessage, SystemMessage } from '@langchain/core/messages'
33
import { SystemMessagePromptTemplate } from '@langchain/core/prompts'
4+
import { END } from '@langchain/langgraph/web'
45
import {
56
createAgentStepsInstructions,
67
CreateGraphOptions,
@@ -15,23 +16,27 @@ import {
1516
} from '@metad/core'
1617
import { injectReferencesRetrieverTool } from 'apps/cloud/src/app/@core/copilot'
1718
import { ChatbiService } from '../../chatbi.service'
18-
import { injectCreateChartTool, injectCreateFormulaTool } from '../tools'
19+
import { injectCreateChartTool } from '../tools'
20+
import { injectCreateIndicatorTool, injectMoreQuestionsTool } from './tools'
1921
import { CHATBI_COMMAND_NAME, insightAgentState } from './types'
2022

2123
export function injectCreateInsightGraph() {
2224
const chatbiService = inject(ChatbiService)
2325
const memberRetrieverTool = injectDimensionMemberTool()
2426
const createChartTool = injectCreateChartTool()
25-
const createFormulaTool = injectCreateFormulaTool()
27+
const createIndicatorTool = injectCreateIndicatorTool()
28+
const moreQuestionsTool = injectMoreQuestionsTool()
2629
const referencesRetrieverTool = injectReferencesRetrieverTool(
2730
[referencesCommandName(CHATBI_COMMAND_NAME), referencesCommandName('calculated')],
2831
{ k: 3 }
2932
)
3033

3134
const context = chatbiService.context
3235

33-
const tools = [referencesRetrieverTool, memberRetrieverTool, createFormulaTool, createChartTool]
3436
return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => {
37+
const indicatorTool = createIndicatorTool(llm)
38+
const tools = [referencesRetrieverTool, memberRetrieverTool, indicatorTool, createChartTool, moreQuestionsTool]
39+
3540
return createReactAgent({
3641
state: insightAgentState,
3742
llm,
@@ -40,34 +45,43 @@ export function injectCreateInsightGraph() {
4045
interruptAfter,
4146
tools: [...tools],
4247
messageModifier: async (state) => {
43-
const systemTemplate = `You are a professional BI data analyst.
48+
const systemTemplate = `You are a professional BI data analyst. Use 'answerQuestion' to ask questions and 'giveMoreQuestions' to get more questions.
4449
{{role}}
4550
{{language}}
4651
47-
Reference Documentations:
48-
{{references}}
49-
5052
The cube context is:
5153
{{context}}
5254
5355
${makeCubeRulesPrompt()}
54-
${PROMPT_RETRIEVE_DIMENSION_MEMBER}
5556
56-
If you have any questions about how to analysis data (such as 'how to create a formula of calculated measure', 'how to create a time slicer about relative time'), please call 'referencesRetriever' tool to get the reference documentations.
57+
If you have any questions about how to analysis data (such as 'how to create some type chart', 'how to create a time slicer about relative time'), please call 'referencesRetriever' tool to get the reference documentations.
5758
5859
${createAgentStepsInstructions(
5960
`Extract the information mentioned in the problem into 'dimensions', 'measurements', 'time', 'slicers', etc.`,
60-
`Determine whether measure exists in the Cube information. If it does, proceed directly to the next step. If not found, call the 'createFormula' tool to create a indicator for that. After creating the indicator, you need to call the subsequent steps to re-answer the complete answer.`,
61+
`For every measure, determine whether it exists in the cube context, if it does, proceed directly to the next step, if not found, call the 'createIndicator' tool to create new calculated measure for it. After creating the measure, you need to call the subsequent steps to re-answer the complete answer.`,
6162
PROMPT_RETRIEVE_DIMENSION_MEMBER,
6263
CubeVariablePrompt,
63-
`Add the time and slicers to slicers in tool, if the measure to be displayed is time-related, add the current period as a filter to the 'timeSlicers'.`,
64-
`Final call 'answerQuestion' tool to answer question, use the complete conditions to answer`
64+
`If the time condition is a specified fixed time (such as 2023 year, 202202, 2020 Q1), please add it to 'slicers' according to the time dimension. If the time condition is relative (such as this month, last month, last year), please add it to 'timeSlicers'.`,
65+
`Then call 'answerQuestion' tool to answer question, use the complete conditions to answer`,
66+
`Then call 'giveMoreQuestions' tool to give more analysis suggestions`
6567
)}
68+
69+
Disable parallel tool calls.
70+
Answer using tools only.
6671
`
6772
const system = await SystemMessagePromptTemplate.fromTemplate(systemTemplate, {
6873
templateFormat: 'mustache'
6974
}).format({ ...state, context: context() })
7075
return [new SystemMessage(system), ...state.messages]
76+
},
77+
toolsRouter: (state) => {
78+
const lastMessage = state.messages[state.messages.length - 1]
79+
if (isAIMessage(lastMessage)) {
80+
if (['giveMoreQuestions'].includes(lastMessage.name)) {
81+
return END
82+
}
83+
}
84+
return 'agent'
7185
}
7286
})
7387
}

0 commit comments

Comments
 (0)