Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): added ai cache capability #18

Merged
merged 43 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6ce5b67
chore: optimize test case
zhoushaw Jul 29, 2024
9088196
chore: optimize test case
zhoushaw Jul 29, 2024
bb0cc0a
chore: optimize report path
zhoushaw Jul 29, 2024
de87143
chore: merge main branch
zhoushaw Jul 29, 2024
210393e
chore: add task cache logic
zhoushaw Jul 29, 2024
e507aee
refactor(report): optimize ai report
zhoushaw Jul 29, 2024
44aa914
chore: save cache logic
zhoushaw Jul 29, 2024
eb75208
chore: modify cache logic
zhoushaw Jul 29, 2024
f12810e
chore: modify cache logic
zhoushaw Jul 29, 2024
d8853bd
chore: modify cache logic
zhoushaw Jul 30, 2024
802cbae
chore: modify unit test case
zhoushaw Jul 30, 2024
665d1dc
chore: modify unit test case
zhoushaw Jul 30, 2024
6368a88
chore: optimize test case
zhoushaw Jul 30, 2024
ba8501c
chore: add cache logic
zhoushaw Jul 30, 2024
280cbae
chore: optimize cache logic
zhoushaw Jul 30, 2024
32e1d04
chore: optimize cache logic
zhoushaw Jul 30, 2024
a0ed3d1
chore: optimize cache logic
zhoushaw Jul 30, 2024
343d7ba
chore: update cache file
zhoushaw Jul 30, 2024
e877130
chore: update cache file
zhoushaw Jul 30, 2024
e8f27fd
chore: update cache file
zhoushaw Jul 30, 2024
ff0cd9a
chore: update cache file
zhoushaw Jul 31, 2024
3744861
chore: Added cache version determination
zhoushaw Jul 31, 2024
3a0982e
chore: Added cache version determination
zhoushaw Jul 31, 2024
208387c
chore: add cache logic
zhoushaw Jul 31, 2024
84a8cb5
refactor(web): use hash replace index number id
zhoushaw Jul 31, 2024
c848aaf
chore: add cache logic
zhoushaw Jul 31, 2024
fea75c3
chore: fix unit test snapshot
zhoushaw Jul 31, 2024
7d0a899
chore: update snapshot
zhoushaw Jul 31, 2024
f760757
chore: update snapshot
zhoushaw Jul 31, 2024
cb2dcdd
chore: update snapshot
zhoushaw Jul 31, 2024
5965deb
chore: update snapshot
zhoushaw Jul 31, 2024
8a01a56
chore: update snapshot
zhoushaw Jul 31, 2024
f418bf4
chore: add cache test
zhoushaw Jul 31, 2024
1a9f90b
chore: optimize test command
zhoushaw Jul 31, 2024
3309cc4
chore: optimize cache logic
zhoushaw Aug 1, 2024
e142ed4
chore: optimize cache logic
zhoushaw Aug 1, 2024
b180707
chore: update snapshot
zhoushaw Aug 1, 2024
e7019c7
chore: update cache
zhoushaw Aug 1, 2024
8412641
chore: delete unless file
zhoushaw Aug 1, 2024
905c5dd
chore: update test data
zhoushaw Aug 1, 2024
a07be19
chore: update test data
zhoushaw Aug 1, 2024
a34f7da
chore: fix build command
zhoushaw Aug 1, 2024
7fb4c5b
chore: update cache logic
zhoushaw Aug 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ blob-report/
playwright/.cache/

# MidScene.js dump files
midscene_run/
midscene-report/
__ai_responses__/


Expand Down
4 changes: 2 additions & 2 deletions packages/midscene/src/ai-model/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function AiInspectElement<ElementType extends BaseElement = BaseEle
context: UIContext<ElementType>;
multi: boolean;
findElementDescription: string;
callAI?: typeof callToGetJSONObject;
callAI?: typeof callToGetJSONObject<AIElementParseResponse>;
}) {
const { context, multi, findElementDescription, callAI = callToGetJSONObject } = options;
const { screenshotBase64 } = context;
Expand All @@ -35,7 +35,7 @@ export async function AiInspectElement<ElementType extends BaseElement = BaseEle
],
},
];
const parseResult = await callAI<AIElementParseResponse>(msgs);
const parseResult = await callAI(msgs);
return {
parseResult,
elementById,
Expand Down
10 changes: 9 additions & 1 deletion packages/midscene/src/ai-model/prompt/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,15 @@ export async function describeUserPage<ElementType extends BaseElement = BaseEle
context: Omit<UIContext<ElementType>, 'describer'>,
) {
const { screenshotBase64 } = context;
const { width, height } = await imageInfoOfBase64(screenshotBase64);
let width: number;
let height: number;

if (context.size) {
({ width, height } = context.size);
} else {
const imgSize = await imageInfoOfBase64(screenshotBase64);
({ width, height } = imgSize);
}

const elementsInfo = context.content;
const idElementMap: Record<string, ElementType> = {};
Expand Down
13 changes: 10 additions & 3 deletions packages/midscene/src/automation/planning.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChatCompletionMessageParam } from 'openai/resources';
import { PlanningAction, PlanningAIResponse, UIContext } from '@/types';
import { callToGetJSONObject as callAI } from '@/ai-model/openai';
import { callToGetJSONObject } from '@/ai-model/openai';
import { describeUserPage } from '@/ai-model';

const characteristic =
Expand Down Expand Up @@ -60,7 +60,14 @@ export function systemPromptToTaskPlanning(query: string) {
`;
}

export async function plan(context: UIContext, userPrompt: string): Promise<{ plans: PlanningAction[] }> {
export async function plan(
userPrompt: string,
opts: {
context: UIContext;
callAI?: typeof callToGetJSONObject<PlanningAIResponse>;
},
): Promise<{ plans: PlanningAction[] }> {
const { callAI = callToGetJSONObject<PlanningAIResponse>, context } = opts || {};
const { screenshotBase64 } = context;
const { description } = await describeUserPage(context);
const systemPrompt = systemPromptToTaskPlanning(userPrompt);
Expand All @@ -84,7 +91,7 @@ export async function plan(context: UIContext, userPrompt: string): Promise<{ pl
},
];

const planFromAI = await callAI<PlanningAIResponse>(msgs);
const planFromAI = await callAI(msgs);
if (planFromAI.error) {
throw new Error(planFromAI.error);
}
Expand Down
33 changes: 22 additions & 11 deletions packages/midscene/src/insight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
BaseElement,
DumpSubscriber,
InsightExtractParam,
AIElementParseResponse,
} from '@/types';

const sortByOrder = (a: UISection, b: UISection) => {
Expand All @@ -26,8 +27,9 @@ const sortByOrder = (a: UISection, b: UISection) => {
}
};

export interface FindElementOptions {
export interface LocateOpts {
multi?: boolean;
callAI?: typeof callAI<AIElementParseResponse>;
}

// export type UnwrapDataShape<T> = T extends EnhancedQuery<infer DataShape> ? DataShape : {};
Expand All @@ -36,19 +38,19 @@ export type AnyValue<T> = {
[K in keyof T]: unknown extends T[K] ? any : T[K];
};

export default class Insight<ElementType extends BaseElement = BaseElement> {
contextRetrieverFn: () => Promise<UIContext<ElementType>> | UIContext<ElementType>;
export default class Insight<
ElementType extends BaseElement = BaseElement,
ContextType extends UIContext<ElementType> = UIContext<ElementType>,
> {
contextRetrieverFn: () => Promise<ContextType> | ContextType;

aiVendorFn: typeof callAI = callAI;

onceDumpUpdatedFn?: DumpSubscriber;

taskInfo?: Omit<InsightTaskInfo, 'durationMs'>;

constructor(
context: UIContext<ElementType> | (() => Promise<UIContext<ElementType>> | UIContext<ElementType>),
opt?: InsightOptions,
) {
constructor(context: ContextType | (() => Promise<ContextType> | ContextType), opt?: InsightOptions) {
assert(context, 'context is required for Insight');
if (typeof context === 'function') {
this.contextRetrieverFn = context;
Expand All @@ -64,19 +66,20 @@ export default class Insight<ElementType extends BaseElement = BaseElement> {
}
}

async locate(queryPrompt: string): Promise<ElementType | null>;
async locate(queryPrompt: string, opt?: { callAI: LocateOpts['callAI'] }): Promise<ElementType | null>;
async locate(queryPrompt: string, opt: { multi: true }): Promise<ElementType[]>;
async locate(queryPrompt: string, opt?: FindElementOptions) {
async locate(queryPrompt: string, opt?: LocateOpts) {
const { callAI = this.aiVendorFn, multi = false } = opt || {};
assert(queryPrompt, 'query is required for located');
const dumpSubscriber = this.onceDumpUpdatedFn;
this.onceDumpUpdatedFn = undefined;
const context = await this.contextRetrieverFn();

const startTime = Date.now();
const { parseResult, systemPrompt, elementById } = await AiInspectElement({
callAI: this.aiVendorFn,
callAI,
context,
multi: Boolean(opt?.multi),
multi: Boolean(multi),
findElementDescription: queryPrompt,
});
// const parseResult = await this.aiVendorFn<AIElementParseResponse>(msgs);
Expand Down Expand Up @@ -282,4 +285,12 @@ export default class Insight<ElementType extends BaseElement = BaseElement> {

return mergedData;
}

setAiVendorFn<T>(aiVendorFn: typeof callAI<T>) {
const origin = this.aiVendorFn;
this.aiVendorFn<T> = aiVendorFn;
return () => {
this.aiVendorFn = origin;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

};
}
}
6 changes: 5 additions & 1 deletion packages/midscene/src/insight/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ export function writeInsightDump(
const length = logContent.push(dataString);
logIdIndexMap[id] = length - 1;
}
writeDumpFile(logFileName, logFileExt, `[\n${logContent.join(',\n')}\n]`);
writeDumpFile({
fileName: logFileName,
fileExt: logFileExt,
fileContent: `[\n${logContent.join(',\n')}\n]`,
});

return id;
}
Expand Down
33 changes: 24 additions & 9 deletions packages/midscene/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,32 @@ export const groupedActionDumpFileExt = 'web-dump.json';
export function getDumpDir() {
return logDir;
}

export function setDumpDir(dir: string) {
logDir = dir;
}

export function writeDumpFile(fileName: string, fileExt: string, fileContent: string) {
export function getDumpDirPath(type: 'dump' | 'cache') {
return join(getDumpDir(), type);
}

export function writeDumpFile(opts: {
fileName: string;
fileExt: string;
fileContent: string;
type?: 'dump' | 'cache';
}) {
const { fileName, fileExt, fileContent, type = 'dump' } = opts;
const targetDir = getDumpDirPath(type);
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
// Ensure directory exists
if (!logEnvReady) {
assert(logDir, 'logDir should be set before writing dump file');
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
assert(targetDir, 'logDir should be set before writing dump file');

// gitIgnore in the parent directory
const gitIgnorePath = join(logDir, '../.gitignore');
const gitIgnorePath = join(targetDir, '../../.gitignore');
let gitIgnoreContent = '';
if (existsSync(gitIgnorePath)) {
gitIgnoreContent = readFileSync(gitIgnorePath, 'utf-8');
Expand All @@ -67,16 +79,19 @@ export function writeDumpFile(fileName: string, fileExt: string, fileContent: st
if (!gitIgnoreContent.includes(`${logDirName}/`)) {
writeFileSync(
gitIgnorePath,
`${gitIgnoreContent}\n# MidScene.js dump files\n${logDirName}/\n`,
`${gitIgnoreContent}\n# MidScene.js dump files\n${logDirName}/midscene-report\n${logDirName}/dump-logger\n`,
'utf-8',
);
}
logEnvReady = true;
}

const filePath = join(getDumpDir(), `${fileName}.${fileExt}`);
const filePath = join(targetDir, `${fileName}.${fileExt}`);
writeFileSync(filePath, fileContent);
copyFileSync(filePath, join(getDumpDir(), `latest.${fileExt}`));

if (type === 'dump') {
copyFileSync(filePath, join(targetDir, `latest.${fileExt}`));
}

return filePath;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"elements": [
{
"id": "2",
"id": "b0ca2e8c69",
},
],
"error": [],
Expand All @@ -11,7 +11,7 @@
{
"elements": [
{
"id": "8",
"id": "b9807d7de6",
},
],
"error": [],
Expand All @@ -20,7 +20,7 @@
{
"elements": [
{
"id": "9",
"id": "c5a7702fed",
},
],
"error": [],
Expand All @@ -29,7 +29,7 @@
{
"elements": [
{
"id": "10",
"id": "c84a3afdac",
},
],
"error": [],
Expand All @@ -38,7 +38,7 @@
{
"elements": [
{
"id": "15",
"id": "defa24dedd",
},
],
"error": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"elements": [
{
"id": "1",
"id": "922e98a196",
},
],
"error": [],
Expand All @@ -11,16 +11,16 @@
{
"elements": [
{
"id": "2",
"id": "83ffa89342",
},
],
"error": [],
"prompt": "Switch language(include:中文、english text)",
"prompt": "Toggle language text button(Could be:中文、english text)",
},
{
"elements": [
{
"id": "4",
"id": "a525985342",
},
],
"error": [],
Expand All @@ -29,10 +29,10 @@
{
"elements": [
{
"id": "22",
"id": "3fb89d359f",
},
{
"id": "28",
"id": "c4300a7c45",
},
],
"error": [],
Expand All @@ -41,10 +41,10 @@
{
"elements": [
{
"id": "23",
"id": "ae0ba24c99",
},
{
"id": "29",
"id": "a50d88f84c",
},
],
"error": [],
Expand All @@ -53,7 +53,7 @@
{
"elements": [
{
"id": "30",
"id": "df4f252aab",
},
],
"error": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from 'vitest';
import path from 'path';
import { test, expect } from 'vitest';
import { getPageTestData, repeat, runTestCases, writeFileSyncWithDir } from './util';
import { AiInspectElement } from '@/ai-model';

Expand All @@ -9,7 +9,7 @@ const testCases = [
multi: false,
},
{
description: 'Switch language(include:中文、english text)',
description: 'Toggle language text button(Could be:中文、english text)',
multi: false,
},
{
Expand All @@ -31,21 +31,28 @@ const testCases = [
];

repeat(5, (repeatIndex) => {
test('xicha: inspect element', async () => {
const { context } = await getPageTestData(path.join(__dirname, './test-data/xicha'));
test(
'xicha: inspect element',
async () => {
const { context } = await getPageTestData(path.join(__dirname, './test-data/online_order'));

const { aiResponse, filterUnStableinf } = await runTestCases(testCases, async (testCase)=>{
const { aiResponse, filterUnStableinf } = await runTestCases(testCases, async (testCase) => {
const { parseResult } = await AiInspectElement({
context,
multi: testCase.multi,
findElementDescription: testCase.description,
});
return parseResult;
});
writeFileSyncWithDir(path.join(__dirname, `__ai_responses__/xicha-inspector-element-${repeatIndex}.json`), JSON.stringify(aiResponse, null, 2), { encoding: 'utf-8'});
expect(filterUnStableinf).toMatchFileSnapshot('./__snapshots__/xicha_inspector.test.ts.snap');
}, {
timeout: 99999,
});
});
writeFileSyncWithDir(
path.join(__dirname, `__ai_responses__/online_order-inspector-element-${repeatIndex}.json`),
JSON.stringify(aiResponse, null, 2),
{ encoding: 'utf-8' },
);
expect(filterUnStableinf).toMatchFileSnapshot('./__snapshots__/online_order_inspector.test.ts.snap');
},
{
timeout: 99999,
},
);
});

Loading
Loading