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

Add caching #179

Merged
merged 25 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
710b172
add caching
gladyshcodes Dec 26, 2024
ee112f9
Merge branch 'main' into feat/cache
gladyshcodes Dec 27, 2024
8aff3ea
run eslint
gladyshcodes Dec 27, 2024
9900099
refine cache file
gladyshcodes Dec 27, 2024
be78be0
update component str retrieval logic
gladyshcodes Dec 27, 2024
3aa7f00
Merge remote-tracking branch 'upstream/main' into feat/cache
gladyshcodes Dec 28, 2024
485cfa8
fix build issue
gladyshcodes Dec 29, 2024
8d23a75
add batch set cache
gladyshcodes Dec 29, 2024
18af38e
implement cache deletion
gladyshcodes Dec 29, 2024
829b40d
fix typo
gladyshcodes Dec 29, 2024
c0fbcdd
add --debug-ai logging for cached tests
gladyshcodes Dec 29, 2024
3c384f1
return window to initial state when cached test fails to execute and …
gladyshcodes Dec 29, 2024
60917ca
self-review refinements
gladyshcodes Dec 29, 2024
63d121d
fix cache settting even when test fails
gladyshcodes Dec 29, 2024
b33be2c
fix build issue
gladyshcodes Dec 29, 2024
b0c2d2c
change cache file path to .shortest
gladyshcodes Dec 29, 2024
3b81b26
Merge remote-tracking branch 'upstream/main' into feat/cache
gladyshcodes Dec 29, 2024
87bbfb2
fix lint issues
gladyshcodes Dec 30, 2024
008461a
fix critical issue
gladyshcodes Dec 30, 2024
f102c29
Merge branch 'main' into feat/cache
slavingia Dec 30, 2024
2dd5914
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 30, 2024
cf6ab19
Update .gitignore
slavingia Dec 30, 2024
02f4fa1
remove duplicate lines
gladyshcodes Dec 30, 2024
11cae11
Merge branch 'feat/cache' of github.com:gladyshcodes/shortest into fe…
gladyshcodes Dec 30, 2024
7f44822
remove .cache dir from gitignore
gladyshcodes Dec 30, 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: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ screenshots/

# tarball
*.tar.gz
*.tgz
*.tgz
63 changes: 54 additions & 9 deletions packages/shortest/src/ai/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Anthropic from "@anthropic-ai/sdk";
import pc from "picocolors";
import { BrowserTool } from "../browser/core/browser-tool";
import { AIConfig } from "../types/ai";
import { CacheAction, CacheStep } from "../types/cache";
import { SYSTEM_PROMPT } from "./prompts";
import { AITools } from "./tools";

Expand Down Expand Up @@ -64,6 +65,8 @@ export class AIClient {
_toolOutputCallback?: (name: string, input: any) => void,
) {
const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [];
// temp cache store
const pendingCache: Partial<{ steps?: CacheStep[] }> = {};

// Log the conversation
if (this.debugMode) {
Expand Down Expand Up @@ -112,18 +115,59 @@ export class AIClient {

// Check for tool use
if (response.stop_reason === "tool_use") {
const toolResults = response.content
.filter((block) => block.type === "tool_use")
.map((block) => {
const toolBlock =
block as Anthropic.Beta.Messages.BetaToolUseBlock;
const toolBlocks: Anthropic.Beta.Messages.BetaToolUseBlock[] =
response.content.filter((block) => block.type === "tool_use");

const toolResults = toolBlocks.map((toolBlock) => {
return {
toolBlock,

result: browserTool.execute(toolBlock.input as any),
};
});

const results = await Promise.all(toolResults.map((t) => t.result));

const getExtras = async (
toolBlock: Anthropic.Beta.Messages.BetaToolUseBlock,
) => {
let extras: any = {};

// @ts-expect-error Incorrect interface on our side leads to this error
// @see https://docs.anthropic.com/en/docs/build-with-claude/computer-use#computer-tool:~:text=%2C%0A%20%20%20%20%20%20%20%20%7D%2C-,%22coordinate%22,-%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22description
if (toolBlock.input.coordinate) {
// @ts-expect-error
const [x, y] = toolBlock.input.coordinate;

const componentStr =
await browserTool.getNormalizedComponentStringByCoords(x, y);

extras = { componentStr };
}

return extras;
};

const newCacheSteps = await Promise.all(
toolBlocks.map(async (_toolBlock, i) => {
const extras = await getExtras(toolBlocks[i]);

return {
toolBlock,
result: browserTool.execute(toolBlock.input as any),
action: toolBlocks[i] as CacheAction,
reasoning: response.content.map(
(block) => (block as any).text,
)[0],
result: results[i].output || null,
extras,
timestamp: Date.now(),
};
});
}),
);

const results = await Promise.all(toolResults.map((t) => t.result));
pendingCache.steps = [
...(pendingCache.steps || []),
...(newCacheSteps || []),
];

// Log tool results
if (this.debugMode) {
Expand Down Expand Up @@ -162,6 +206,7 @@ export class AIClient {
return {
messages,
finalResponse: response,
pendingCache,
};
}
} catch (error: any) {
Expand Down
91 changes: 91 additions & 0 deletions packages/shortest/src/browser/core/browser-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,4 +754,95 @@ export class BrowserTool extends BaseBrowserTool {
if (trail) trail.style.display = "none";
});
}

/**
* Retrieves normalized component string by X and Y coordinates
* This is primarily used to determine change in UI
* Playwright currently does not support such functionality
* @see https://github.com/microsoft/playwright/issues/13273
*/
async getNormalizedComponentStringByCoords(x: number, y: number) {
return await this.getPage().evaluate(
({ x, y, allowedAttr }) => {
const elem = document.elementFromPoint(x, y);
if (elem) {
// todo: test func below
const clone = elem.cloneNode(true) as HTMLElement;

/**
* Gets deepest nested child node
* If several nodes are on the same depth, the first node would be returned
*/
function getDeepestChildNode(element: Element): HTMLElement {
let deepestChild = element.cloneNode(true) as HTMLElement;
let maxDepth = 0;

function traverse(node: any, depth: number) {
if (depth > maxDepth) {
maxDepth = depth;
deepestChild = node;
}

Array.from(node.children).forEach((child) => {
traverse(child, depth + 1);
});
}

traverse(deepestChild, 0);
return deepestChild;
}

const deepestNode = getDeepestChildNode(clone);

// get several parents if present
const node = deepestNode.parentElement
? deepestNode.parentElement.parentElement
? deepestNode.parentElement.parentElement
: deepestNode.parentElement
: deepestNode;

/**
* Recursively delete attributes from Nodes
*/
function cleanAttributesRecursively(
element: Element,
options: { exceptions: string[] },
) {
Array.from(element.attributes).forEach((attr) => {
if (!options.exceptions.includes(attr.name)) {
element.removeAttribute(attr.name);
}
});

Array.from(element.children).forEach((child) => {
cleanAttributesRecursively(child, options);
});
}

cleanAttributesRecursively(node, {
exceptions: allowedAttr,
});

// trim and remove white spaces
return node.outerHTML.trim().replace(/\s+/g, " ");
} else {
return "";
}
},
{
x,
y,
allowedAttr: [
"type",
"name",
"placeholder",
"aria-label",
"role",
"title",
"alt",
"d", // for <path> tags
],
},
);
}
}
Loading
Loading