Skip to content

Commit

Permalink
Merge pull request #40 from meta-d/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
meta-d authored Jul 19, 2024
2 parents ad053f0 + f740b69 commit 9978a5d
Show file tree
Hide file tree
Showing 60 changed files with 523 additions and 186 deletions.
2 changes: 1 addition & 1 deletion .deploy/api/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ocap-server",
"author": "Metad",
"version": "2.4.6",
"version": "2.4.7",
"scripts": {
"start": "nx serve",
"build": "nx build",
Expand Down
2 changes: 1 addition & 1 deletion .deploy/webapp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ocap",
"version": "2.4.6",
"version": "2.4.7",
"description": "",
"license": "AGPL-3.0",
"homepage": "https://mtda.cloud",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { inject } from '@angular/core'
import { CopilotAgentType } 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 { injectCreateIndicatorArchitect } from './graph'
import { INDICATOR_AGENT_NAME, PLANNER_NAME, REPLANNER_NAME } from './types'
import { INDICATOR_AGENT_NAME, IndicatorArchitectCommandName, PLANNER_NAME, REPLANNER_NAME } from './types'

export function injectIndicatorArchitectCommand() {
const logger = inject(NGXLogger)
const translate = inject(TranslateService)

const fewShotPrompt = injectAgentFewShotTemplate(IndicatorArchitectCommandName, { k: 1, vectorStore: null })
const createIndicatorGraph = injectCreateIndicatorArchitect()

// const indicators = computed(() => projectService.indicators() ?? [])
Expand Down Expand Up @@ -39,8 +42,9 @@ export function injectIndicatorArchitectCommand() {
agent: {
type: CopilotAgentType.Graph,
conversation: true,
interruptAfter: [PLANNER_NAME, REPLANNER_NAME, INDICATOR_AGENT_NAME]
interruptAfter: [ INDICATOR_AGENT_NAME ]
},
fewShotPrompt,
createGraph: createIndicatorGraph
})
}
125 changes: 86 additions & 39 deletions apps/cloud/src/app/features/project/copilot/architect/graph.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,115 @@
import { computed, inject } from '@angular/core'
import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web'
import { CreateGraphOptions } from '@metad/copilot'
import { Plan, injectAgentFewShotTemplate } from '../../../../@core/copilot/'
import { computed, inject, Signal } from '@angular/core'
import { ToolMessage } from '@langchain/core/messages'
import { RunnableLambda } from '@langchain/core/runnables'
import { DynamicStructuredTool } from '@langchain/core/tools'
import { ToolNode } from '@langchain/langgraph/prebuilt'
import { START, StateGraph, StateGraphArgs } from '@langchain/langgraph/web'
import { ChatOpenAI } from '@langchain/openai'
import { Indicator } from '@metad/cloud/state'
import { CreateGraphOptions, Team } from '@metad/copilot'
import { injectDimensionMemberTool } from '@metad/core'
import { ProjectService } from '../../project.service'
import { injectRunIndicatorAgent } from '../indicator/graph'
import { createPlannerAgent } from './agent-planner'
import { createReplannerAgent } from './agent-replanner'
import {
INDICATOR_AGENT_NAME,
IndicatorArchitectCommandName,
IndicatorArchitectState,
PLANNER_NAME,
REPLANNER_NAME
} from './types'

const superState: StateGraphArgs<IndicatorArchitectState>['channels'] = Plan.createState()
import { promptIndicatorCode } from '../prompt'
import { INDICATOR_AGENT_NAME, IndicatorArchitectState, markdownIndicators } from './types'

const superState: StateGraphArgs<IndicatorArchitectState>['channels'] = Team.createState()

export function injectCreateIndicatorArchitect() {
const fewShotTemplate = injectAgentFewShotTemplate(IndicatorArchitectCommandName, { k: 1, vectorStore: null })
const projectService = inject(ProjectService)
const memberRetrieverTool = injectDimensionMemberTool()
const createIndicatorGraph = injectRunIndicatorAgent()

const indicators = computed(() => projectService.indicators() ?? [])

return async ({ llm, checkpointer, interruptBefore, interruptAfter}: CreateGraphOptions) => {
return async ({ llm, checkpointer, interruptBefore, interruptAfter }: CreateGraphOptions) => {
const indicatorWorker = await createIndicatorGraph({ llm })

const planner = await createPlannerAgent({ llm, fewShotTemplate, indicators })
const replanner = await createReplannerAgent({ llm })
// const planner = await createPlannerAgent({ llm, fewShotTemplate, indicators })
// const replanner = await createReplannerAgent({ llm })
const tools = [memberRetrieverTool]
const supervisorAgent = await createSupervisorAgent({ llm, indicators, tools: [] })

async function executeStep(state: IndicatorArchitectState): Promise<any> {
const task = state.plan[0]

const { messages } = await indicatorWorker.invoke({
messages: [],
role: state.role,
context: state.context,
input: task
input: state.instructions,
})

return {
pastSteps: [[task, messages[messages.length - 1].content.toString()]],
plan: state.plan.slice(1),
messages: [messages[messages.length - 1]]
tool_call_id: null,
messages: [
new ToolMessage({
tool_call_id: state.tool_call_id,
content: messages[messages.length - 1].content
})
]
}
}

function shouldEnd(state: IndicatorArchitectState) {
return state.response ? 'true' : 'false'
}

const superGraph = new StateGraph({ channels: superState })
// Add steps nodes
.addNode(PLANNER_NAME, planner)
.addNode(Team.SUPERVISOR_NAME, supervisorAgent.withConfig({ runName: Team.SUPERVISOR_NAME }))
.addNode(Team.TOOLS_NAME, new ToolNode<IndicatorArchitectState>(tools))
.addNode(INDICATOR_AGENT_NAME, executeStep)
.addNode(REPLANNER_NAME, replanner)
.addEdge(START, PLANNER_NAME)
.addEdge(PLANNER_NAME, INDICATOR_AGENT_NAME)
.addEdge(INDICATOR_AGENT_NAME, REPLANNER_NAME)
.addConditionalEdges(REPLANNER_NAME, shouldEnd, {
true: END,
false: INDICATOR_AGENT_NAME
})
.addEdge(START, Team.SUPERVISOR_NAME)
.addConditionalEdges(Team.SUPERVISOR_NAME, Team.supervisorRouter)
.addEdge(INDICATOR_AGENT_NAME, Team.SUPERVISOR_NAME)
.addEdge(Team.TOOLS_NAME, Team.SUPERVISOR_NAME)

return superGraph.compile({ checkpointer, interruptBefore, interruptAfter})
return superGraph.compile({ checkpointer, interruptBefore, interruptAfter })
}
}

export async function createSupervisorAgent({
llm,
indicators,
tools
}: {
llm: ChatOpenAI
indicators: Signal<Indicator[]>
tools: DynamicStructuredTool[]
}) {
const agent = await Team.createSupervisorAgent(
llm,
[
{
name: INDICATOR_AGENT_NAME,
description: 'Create an indicator, only one at a time'
}
],
tools,
`As a indicator system architect specializing in data analysis, your task is to develop a set of indicators specifically for business data analysis based on multidimensional cube information and user prompts, and align with your business role.
Each indicator gives a concise business requirement and name, and the indicators are sorted in the order of creation dependencies.
{role}
{language}
{context}
Methods for indicator design:
- Directly use the basic measures defined in the model, which are measurement data extracted directly from the data source, such as sales amount, inventory quantity, etc.
- Design indicators with calculation formulas, which are indicators calculated based on basic measures, such as year-on-year growth rate, average inventory turnover rate, etc.
- Use Restricted filters to limit measurement data according to specific conditions or dimensions, such as sales in a specific time period, inventory in a certain area, etc.
1. Do not create duplicate indicators that already exist:
{indicators}
2. ${promptIndicatorCode(`{indicatorCodes}`)}
3. Please plan the indicator system first, and then decide to call route to create it one by one.
`
)

return RunnableLambda.from(async (state: IndicatorArchitectState) => {
return {
...state,
indicators: markdownIndicators(indicators()),
indicatorCodes: indicators()
.map((indicator) => indicator.code)
.join(', '),
}
}).pipe(agent)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Indicator } from '@metad/cloud/state'
import { Plan } from 'apps/cloud/src/app/@core/copilot'
import { Team } from '@metad/copilot'

export const IndicatorArchitectCommandName = 'indicator-architect'
export const PLANNER_NAME = 'Planner'
Expand All @@ -8,11 +8,10 @@ export const REPLANNER_NAME = 'Replanner'
export const INDICATOR_AGENT_NAME = 'IndicatorAgent'

// Define the top-level State interface
export interface IndicatorArchitectState extends Plan.State {
}
export interface IndicatorArchitectState extends Team.State {}

export function markdownIndicators(indicators: Indicator[]) {
return indicators
.map((indicator) => ` -name: ${indicator.name}\n code: ${indicator.code}\n business: ${indicator.business || ''}`)
.join('\n')
.join('\n') || 'Empty'
}
15 changes: 14 additions & 1 deletion apps/cloud/src/app/features/project/copilot/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgmCopilotContextToken } from '@metad/copilot-angular'
import { markdownEntityType } from '@metad/core'
import { firstValueFrom, map, shareReplay } from 'rxjs'
import { ProjectService } from '../project.service'
import { isIndicatorMeasureProperty } from '@metad/ocap-core'

export function provideCopilotCubes() {
const projectService = inject(ProjectService)
Expand All @@ -19,7 +20,19 @@ export function provideCopilotCubes() {
dataSourceId: model.id,
serizalize: async () => {
const entityType = await firstValueFrom(projectService.selectEntityType(model.key, cube.name))
return `The model id: '${model.id}'\n` + markdownEntityType(entityType)
if (entityType) {

}
return `The model id: '${model.id}'\n` + markdownEntityType({
...entityType,
// Filter excludes indicators
properties: Object.keys(entityType.properties)
.filter((key) => !isIndicatorMeasureProperty(entityType.properties[key]))
.reduce((properties, key) => {
properties[key] = entityType.properties[key]
return properties
}, {})
})
}
},
key: cube.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DynamicStructuredTool } from '@langchain/core/tools'
import { makeCubeRulesPrompt, MEMBER_RETRIEVER_TOOL_NAME } from '@metad/core'
import { Route } from '../../../../@core/copilot'
import { markdownBusinessAreas, markdownTags } from '../schema'
import { promptIndicatorCode } from '../prompt'

export async function createIndicatorWorker(
{ llm, indicatorCodes, businessAreas, tags },
Expand All @@ -11,13 +12,14 @@ export async function createIndicatorWorker(
`You are a business expert in BI indicator system management. Please convert the specified Cube information and requirement description into corresponding parameters and call the createIndicator tool to create a new indicator.` +
`\n{{role}}\n` +
`\n${makeCubeRulesPrompt()}` +
`\n1. Code cannot be the same as the following existing ones: [${indicatorCodes().join(', ')}]` +
`\n2. Specify a hierarchy name (not the level name) of calendar dimension for this indicator to be used for future calculations of the indicator's trends at different time granularity. If no calendar semantic dimension is found in cube, this question needs to be answered.` +
`\n3. If the requirement specifies the member condition of the dimension to be limited, then determine which dimension and member description needs to be limited based on the cube dimension information. Call the '${MEMBER_RETRIEVER_TOOL_NAME}' tool to obtain the accurate information of the dimension member and add it to the filters.` +
`\n4. First, select a suitable measure from the Measures of the Cube as the measure field for defining the basic type of indicator. If the measure field of the basic indicator cannot meet the requirements, consider creating an MDX formula of calculated measure as the formula for the derived indicator. You don't need to multiply by 100 when defining a percentage formula` +
`\n5. Set all dimensions (not hierarchy) not used in filters or formula or calendar to the 'dimensions' field.` +
`\n6. If the indicator value is a ratio or percentage, you need to set unit to '%'.` +
`\n7. If the cube has variables then all variables with defaultValueKey are added to the indicator's variables property, where each variable has the format:
`
1. ${promptIndicatorCode(indicatorCodes().join(', '))}
2. Specifies the hierarchy name (not the level name) of the calendar dimension for this metric, which is used to calculate the metric trend at different time granularities in the future, if you can find a hierarchy of calendar dimension with multiple time granularities levels.
3. If the requirement specifies the member condition of the dimension to be limited, then determine which dimension and member description needs to be limited based on the cube dimension information. Call the '${MEMBER_RETRIEVER_TOOL_NAME}' tool to obtain the accurate information of the dimension member and add it to the filters.
4. First, select a suitable measure from the Measures of the Cube as the measure field for defining the basic type of indicator. If the measure field of the basic indicator cannot meet the requirements, consider creating an MDX formula of calculated measure as the formula for the derived indicator. You don't need to multiply by 100 when defining a percentage formula
5. Set all dimensions (not hierarchy) not used in filters or formula or calendar to the 'dimensions' field.
6. If the indicator value is a ratio or percentage, you need to set unit to '%'.
7. If the cube has variables then all variables with defaultValueKey are added to the indicator's variables property, where each variable has the format:
{
dimension: {
dimension: variable.referenceDimension,
Expand All @@ -30,12 +32,14 @@ export async function createIndicatorWorker(
caption: variable.defaultValueCaption
}
]
}.` +
`\n8. Select the appropriate Business Areas from the following to fill in the businessAreaId field:` +
markdownBusinessAreas(businessAreas()) +
`\n9. Select the relevant tags from the following and fill in the tags field:` +
markdownTags(tags()) +
`\n If no Cube information is provided or you need to reselect a Cube, please call the 'pickCube' tool to get the Cube information.` +
}.
8. Select the appropriate Business Areas from the following to fill in the businessAreaId field:
${markdownBusinessAreas(businessAreas())}
9. Select the relevant tags from the following and fill in the tags field:
${markdownTags(tags())}
If no Cube information is provided or you need to reselect a Cube, please call the 'pickCube' tool to get the Cube information.
` +

`\n{{context}}` +
`\nCurrent indicator info is:` +
`\n{{indicator}}`
Expand Down
6 changes: 6 additions & 0 deletions apps/cloud/src/app/features/project/copilot/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function promptIndicatorCode(indicatorCodes: string) {
return `Indicator code rules:
a. Cannot be the same as the following existing ones: [${indicatorCodes}]
b. Cannot be the same as the name of measures in the cube
c. The code uses a unified coding rule, for example, indicators belonging to the same business module use the same code prefix.`
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ngm-table class="flex-1 overflow-hidden rounded-lg border mt-2"
<ngm-table class="flex-1 overflow-hidden rounded-lg mt-2"
paging
[data]="approvals$ | async"
[columns]="[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:host {
@apply flex-1 w-full max-w-full overflow-hidden flex flex-col p-4;
@apply flex-1 w-full max-w-full overflow-hidden flex flex-col;
}

::ng-deep {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
ChartDimensionRoleType,
DataSettings,
FilterOperator,
getEntityCalendar,
getIndicatorEntityCalendar,
IFilter,
isEqual,
negate,
Expand Down Expand Up @@ -119,15 +119,27 @@ export class IndicatorRegisterComponent extends TranslationBaseComponent impleme
if (!entityType) {
return null
}
const calendar = getEntityCalendar(entityType, indicator.calendar, timeGranularity)
if (!calendar) {

// const calendar = getEntityCalendar(entityType, indicator.calendar, timeGranularity)
// if (!calendar) {
// return {
// error: this.translateService.instant(`PAC.INDICATOR.REGISTER.CalendarDimensionNotSet`, {
// Default: 'Calendar dimension not set'
// })
// } as undefined as DataSettings & { error?: string }
// }
const { dimension, hierarchy, level } = getIndicatorEntityCalendar(
indicator,
entityType,
timeGranularity
)
if (!level) {
return {
error: this.translateService.instant(`PAC.INDICATOR.REGISTER.CalendarDimensionNotSet`, {
Default: 'Calendar dimension not set'
})
} as undefined as DataSettings & { error?: string }
}
const { dimension, hierarchy, level } = calendar

const timeRange = calcRange(new Date(), {
type: TimeRangeType.Standard,
Expand All @@ -145,7 +157,7 @@ export class IndicatorRegisterComponent extends TranslationBaseComponent impleme
operator: FilterOperator.BT
} as IFilter

return dataSettings && calendar
return dataSettings && level
? ({
...dataSettings,
chartAnnotation: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<mat-toolbar class="pac-nav-toolbar flex" displayDensity="compact">
@if (!modelSideMenuOpened()) {
<button mat-icon-button class="text-neutral-600" (click)="openSideMenu()">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" class="icon-xl-heavy">
<path fill="currentColor" fill-rule="evenodd" d="M8.857 3h6.286c1.084 0 1.958 0 2.666.058.729.06 1.369.185 1.961.487a5 5 0 0 1 2.185 2.185c.302.592.428 1.233.487 1.961.058.708.058 1.582.058 2.666v3.286c0 1.084 0 1.958-.058 2.666-.06.729-.185 1.369-.487 1.961a5 5 0 0 1-2.185 2.185c-.592.302-1.232.428-1.961.487C17.1 21 16.227 21 15.143 21H8.857c-1.084 0-1.958 0-2.666-.058-.728-.06-1.369-.185-1.96-.487a5 5 0 0 1-2.186-2.185c-.302-.592-.428-1.232-.487-1.961C1.5 15.6 1.5 14.727 1.5 13.643v-3.286c0-1.084 0-1.958.058-2.666.06-.728.185-1.369.487-1.96A5 5 0 0 1 4.23 3.544c.592-.302 1.233-.428 1.961-.487C6.9 3 7.773 3 8.857 3M6.354 5.051c-.605.05-.953.142-1.216.276a3 3 0 0 0-1.311 1.311c-.134.263-.226.611-.276 1.216-.05.617-.051 1.41-.051 2.546v3.2c0 1.137 0 1.929.051 2.546.05.605.142.953.276 1.216a3 3 0 0 0 1.311 1.311c.263.134.611.226 1.216.276.617.05 1.41.051 2.546.051h.6V5h-.6c-1.137 0-1.929 0-2.546.051M11.5 5v14h3.6c1.137 0 1.929 0 2.546-.051.605-.05.953-.142 1.216-.276a3 3 0 0 0 1.311-1.311c.134-.263.226-.611.276-1.216.05-.617.051-1.41.051-2.546v-3.2c0-1.137 0-1.929-.051-2.546-.05-.605-.142-.953-.276-1.216a3 3 0 0 0-1.311-1.311c-.263-.134-.611-.226-1.216-.276C17.029 5.001 16.236 5 15.1 5zM5 8.5a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1M5 12a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1" clip-rule="evenodd"></path>
</svg>
</button>
}
<nav *ngIf="roles?.length" mat-tab-nav-bar color="accent" disableRipple mat-stretch-tabs="false" mat-align-tabs="start"
class="pac-model-access__nav flex-1"
[tabPanel]="tabPanel"
Expand Down
Loading

0 comments on commit 9978a5d

Please sign in to comment.