-
Notifications
You must be signed in to change notification settings - Fork 5
Feat(templates): add lead-scraper python template #101
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
Open
arthursita-plank
wants to merge
10
commits into
kernel:main
Choose a base branch
from
arthursita-plank:feat/lead-scraper
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5d470f0
feat: lead-scraper template
arthursita-plank 8970c11
chore: change the model version
arthursita-plank 384aa3f
chore: fix cursor bugbot comment
arthursita-plank 0cfb74d
feat: add template name to readme documentation
arthursita-plank 359a031
feat: scrape leads from any website
arthursita-plank f60a5ee
refactor: implement anthropic computer use and remove browser use
arthursita-plank 3b1d02d
feat: add replay option on payload
arthursita-plank 7e7325a
fix: cursor comments
arthursita-plank d12bf7f
fix: more cursor comments
arthursita-plank 9bac631
docs(template): add readme documentation and fix bot comments
arthursita-plank File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Lead Scraper Environment Variables | ||
| # Copy this to .env and fill in the values | ||
|
|
||
| ANTHROPIC_API_KEY=your_anthropic_api_key_here |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,336 @@ | ||
| """ | ||
| Agentic sampling loop that calls the Anthropic API and local implementation of anthropic-defined computer use tools. | ||
| From https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py | ||
| Modified to use Kernel Computer Controls API instead of Playwright. | ||
| """ | ||
|
|
||
| import os | ||
| from datetime import datetime | ||
| from enum import StrEnum | ||
| from typing import Any, cast | ||
|
|
||
| from kernel import Kernel | ||
| from anthropic import Anthropic | ||
| from anthropic.types.beta import ( | ||
| BetaCacheControlEphemeralParam, | ||
| BetaContentBlockParam, | ||
| BetaImageBlockParam, | ||
| BetaMessage, | ||
| BetaMessageParam, | ||
| BetaTextBlock, | ||
| BetaTextBlockParam, | ||
| BetaToolResultBlockParam, | ||
| BetaToolUseBlockParam, | ||
| ) | ||
|
|
||
| from tools import ( | ||
| TOOL_GROUPS_BY_VERSION, | ||
| ToolCollection, | ||
| ToolResult, | ||
| ToolVersion, | ||
| ) | ||
|
|
||
| PROMPT_CACHING_BETA_FLAG = "prompt-caching-2024-07-31" | ||
|
|
||
|
|
||
| class APIProvider(StrEnum): | ||
| ANTHROPIC = "anthropic" | ||
|
|
||
|
|
||
| # This system prompt is optimized for the Docker environment in this repository and | ||
| # specific tool combinations enabled. | ||
| # We encourage modifying this system prompt to ensure the model has context for the | ||
| # environment it is running in, and to provide any additional information that may be | ||
| # helpful for the task at hand. | ||
| SYSTEM_PROMPT = f"""<SYSTEM_CAPABILITY> | ||
| * You are utilising an Ubuntu virtual machine using {os.uname().machine} architecture with internet access. | ||
| * When you connect to the display, CHROMIUM IS ALREADY OPEN. The url bar is not visible but it is there. | ||
| * If you need to navigate to a new page, use ctrl+l to focus the url bar and then enter the url. | ||
| * You won't be able to see the url bar from the screenshot but ctrl-l still works. | ||
| * As the initial step click on the search bar. | ||
| * When viewing a page it can be helpful to zoom out so that you can see everything on the page. | ||
| * Either that, or make sure you scroll down to see everything before deciding something isn't available. | ||
| * When using your computer function calls, they take a while to run and send back to you. | ||
| * Where possible/feasible, try to chain multiple of these calls all into one function calls request. | ||
| * The current date is {datetime.now().strftime("%A, %B %d, %Y")}. | ||
| * After each step, take a screenshot and carefully evaluate if you have achieved the right outcome. | ||
| * Explicitly show your thinking: "I have evaluated step X..." If not correct, try again. | ||
| * Only when you confirm a step was executed correctly should you move on to the next one. | ||
| </SYSTEM_CAPABILITY> | ||
|
|
||
| <IMPORTANT> | ||
| * When using Chromium, if a startup wizard appears, IGNORE IT. Do not even click "skip this step". | ||
| * Instead, click on the search bar on the center of the screen where it says "Search or enter address", and enter the appropriate search term or URL there. | ||
| </IMPORTANT>""" | ||
|
|
||
|
|
||
| async def sampling_loop( | ||
| *, | ||
| model: str, | ||
| messages: list[BetaMessageParam], | ||
| api_key: str, | ||
| kernel: Kernel, | ||
| session_id: str, | ||
| system_prompt_suffix: str = "", | ||
| only_n_most_recent_images: int | None = None, | ||
| max_tokens: int = 4096, | ||
| tool_version: ToolVersion = "computer_use_20250124", | ||
| thinking_budget: int | None = None, | ||
| token_efficient_tools_beta: bool = False, | ||
| ): | ||
| """ | ||
| Agentic sampling loop for the assistant/tool interaction of computer use. | ||
|
|
||
| Args: | ||
| model: The model to use for the API call | ||
| messages: The conversation history | ||
| api_key: The API key for authentication | ||
| kernel: The Kernel client instance | ||
| session_id: The Kernel browser session ID | ||
| provider: The API provider (defaults to ANTHROPIC) | ||
| system_prompt_suffix: Additional system prompt text (defaults to empty string) | ||
| only_n_most_recent_images: Optional limit on number of recent images to keep | ||
| max_tokens: Maximum tokens for the response (defaults to 4096) | ||
| tool_version: Version of tools to use (defaults to V20250124) | ||
| thinking_budget: Optional token budget for thinking | ||
| token_efficient_tools_beta: Whether to use token efficient tools beta | ||
| """ | ||
| tool_group = TOOL_GROUPS_BY_VERSION[tool_version] | ||
| tool_collection = ToolCollection( | ||
| *( | ||
| ToolCls(kernel=kernel, session_id=session_id) if ToolCls.__name__.startswith("ComputerTool") else ToolCls() | ||
| for ToolCls in tool_group.tools | ||
| ) | ||
| ) | ||
| system = BetaTextBlockParam( | ||
| type="text", | ||
| text=f"{SYSTEM_PROMPT}{' ' + system_prompt_suffix if system_prompt_suffix else ''}", | ||
| ) | ||
|
|
||
| while True: | ||
| betas = [tool_group.beta_flag] if tool_group.beta_flag else [] | ||
| if token_efficient_tools_beta: | ||
| betas.append("token-efficient-tools-2025-02-19") | ||
| image_truncation_threshold = only_n_most_recent_images or 0 | ||
| client = Anthropic(api_key=api_key, max_retries=4) | ||
|
|
||
| betas.append(PROMPT_CACHING_BETA_FLAG) | ||
| _inject_prompt_caching(messages) | ||
| # Because cached reads are 10% of the price, we don't think it's | ||
| # ever sensible to break the cache by truncating images | ||
| only_n_most_recent_images = 0 | ||
| # Use type ignore to bypass TypedDict check until SDK types are updated | ||
| system["cache_control"] = {"type": "ephemeral"} # type: ignore | ||
|
|
||
| if only_n_most_recent_images: | ||
| _maybe_filter_to_n_most_recent_images( | ||
| messages, | ||
| only_n_most_recent_images, | ||
| min_removal_threshold=image_truncation_threshold, | ||
| ) | ||
arthursita-plank marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| extra_body = {} | ||
| if thinking_budget: | ||
| # Ensure we only send the required fields for thinking | ||
| extra_body = { | ||
| "thinking": {"type": "enabled", "budget_tokens": thinking_budget} | ||
| } | ||
|
|
||
| # Call the API | ||
| response = client.beta.messages.create( | ||
| max_tokens=max_tokens, | ||
| messages=messages, | ||
| model=model, | ||
| system=[system], | ||
| tools=tool_collection.to_params(), | ||
| betas=betas, | ||
| extra_body=extra_body, | ||
| ) | ||
|
|
||
| response_params = _response_to_params(response) | ||
| messages.append( | ||
| { | ||
| "role": "assistant", | ||
| "content": response_params, | ||
| } | ||
| ) | ||
|
|
||
| loggable_content = [ | ||
| { | ||
| "text": block.get("text", "") or block.get("thinking", ""), | ||
| "input": block.get("input", ""), | ||
| } | ||
| for block in response_params | ||
| ] | ||
| print("=== LLM RESPONSE ===") | ||
| print("Stop reason:", response.stop_reason) | ||
| print(loggable_content) | ||
| print("===") | ||
|
|
||
| if response.stop_reason == "end_turn": | ||
| print("LLM has completed its task, ending loop") | ||
| return messages | ||
|
|
||
| tool_result_content: list[BetaToolResultBlockParam] = [] | ||
| for content_block in response_params: | ||
| if content_block["type"] == "tool_use": | ||
| result = await tool_collection.run( | ||
| name=content_block["name"], | ||
| tool_input=cast(dict[str, Any], content_block["input"]), | ||
| ) | ||
| tool_result_content.append( | ||
| _make_api_tool_result(result, content_block["id"]) | ||
| ) | ||
|
|
||
| if not tool_result_content: | ||
| return messages | ||
|
|
||
| messages.append({"content": tool_result_content, "role": "user"}) | ||
|
|
||
|
|
||
| def _maybe_filter_to_n_most_recent_images( | ||
| messages: list[BetaMessageParam], | ||
| images_to_keep: int, | ||
| min_removal_threshold: int, | ||
| ): | ||
| """ | ||
| With the assumption that images are screenshots that are of diminishing value as | ||
| the conversation progresses, remove all but the final `images_to_keep` tool_result | ||
| images in place, with a chunk of min_removal_threshold to reduce the amount we | ||
| break the implicit prompt cache. | ||
| """ | ||
| if images_to_keep is None: | ||
| return messages | ||
|
|
||
| tool_result_blocks = cast( | ||
| list[BetaToolResultBlockParam], | ||
| [ | ||
| item | ||
| for message in messages | ||
| for item in ( | ||
| message["content"] if isinstance(message["content"], list) else [] | ||
| ) | ||
| if isinstance(item, dict) and item.get("type") == "tool_result" | ||
| ], | ||
| ) | ||
|
|
||
| total_images = sum( | ||
| 1 | ||
| for tool_result in tool_result_blocks | ||
| for content in tool_result.get("content", []) | ||
| if isinstance(content, dict) and content.get("type") == "image" | ||
| ) | ||
|
|
||
| images_to_remove = total_images - images_to_keep | ||
| # for better cache behavior, we want to remove in chunks | ||
| images_to_remove -= images_to_remove % min_removal_threshold | ||
|
|
||
| for tool_result in tool_result_blocks: | ||
| if isinstance(tool_result.get("content"), list): | ||
| new_content = [] | ||
| for content in tool_result.get("content", []): | ||
| if isinstance(content, dict) and content.get("type") == "image": | ||
| if images_to_remove > 0: | ||
| images_to_remove -= 1 | ||
| continue | ||
| new_content.append(content) | ||
| tool_result["content"] = new_content | ||
|
|
||
arthursita-plank marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def _response_to_params( | ||
| response: BetaMessage, | ||
| ) -> list[BetaContentBlockParam]: | ||
| res: list[BetaContentBlockParam] = [] | ||
| for block in response.content: | ||
| block_type = getattr(block, "type", None) | ||
|
|
||
| if block_type == "thinking": | ||
| thinking_block = { | ||
| "type": "thinking", | ||
| "thinking": getattr(block, "thinking", None), | ||
| } | ||
| if hasattr(block, "signature"): | ||
| thinking_block["signature"] = getattr(block, "signature", None) | ||
| res.append(cast(BetaContentBlockParam, thinking_block)) | ||
| elif block_type == "text" or isinstance(block, BetaTextBlock): | ||
| if getattr(block, "text", None): | ||
| res.append(BetaTextBlockParam(type="text", text=block.text)) | ||
| elif block_type == "tool_use": | ||
| tool_use_block: BetaToolUseBlockParam = { | ||
| "type": "tool_use", | ||
| "id": block.id, | ||
| "name": block.name, | ||
| "input": block.input, | ||
| } | ||
| res.append(tool_use_block) | ||
| else: | ||
| # Preserve unexpected block types to avoid silently dropping content | ||
| if hasattr(block, "model_dump"): | ||
| res.append(cast(BetaContentBlockParam, block.model_dump())) | ||
| return res | ||
|
|
||
|
|
||
| def _inject_prompt_caching( | ||
| messages: list[BetaMessageParam], | ||
| ): | ||
| """ | ||
| Set cache breakpoints for the 3 most recent turns | ||
| one cache breakpoint is left for tools/system prompt, to be shared across sessions | ||
| """ | ||
|
|
||
| breakpoints_remaining = 3 | ||
| for message in reversed(messages): | ||
| if message["role"] == "user" and isinstance( | ||
| content := message["content"], list | ||
| ): | ||
| if breakpoints_remaining: | ||
| breakpoints_remaining -= 1 | ||
| # Use type ignore to bypass TypedDict check until SDK types are updated | ||
| content[-1]["cache_control"] = BetaCacheControlEphemeralParam( # type: ignore | ||
| {"type": "ephemeral"} | ||
| ) | ||
| else: | ||
| content[-1].pop("cache_control", None) | ||
| # we'll only every have one extra turn per loop | ||
| break | ||
|
|
||
|
|
||
| def _make_api_tool_result( | ||
| result: ToolResult, tool_use_id: str | ||
| ) -> BetaToolResultBlockParam: | ||
| """Convert an agent ToolResult to an API ToolResultBlockParam.""" | ||
| tool_result_content: list[BetaTextBlockParam | BetaImageBlockParam] | str = [] | ||
| is_error = False | ||
| if result.error: | ||
| is_error = True | ||
| tool_result_content = _maybe_prepend_system_tool_result(result, result.error) | ||
| else: | ||
| if result.output: | ||
| tool_result_content.append( | ||
| { | ||
| "type": "text", | ||
| "text": _maybe_prepend_system_tool_result(result, result.output), | ||
| } | ||
| ) | ||
| if result.base64_image: | ||
| tool_result_content.append( | ||
| { | ||
| "type": "image", | ||
| "source": { | ||
| "type": "base64", | ||
| "media_type": "image/png", | ||
| "data": result.base64_image, | ||
| }, | ||
| } | ||
| ) | ||
| return { | ||
| "type": "tool_result", | ||
| "content": tool_result_content, | ||
| "tool_use_id": tool_use_id, | ||
| "is_error": is_error, | ||
| } | ||
|
|
||
|
|
||
| def _maybe_prepend_system_tool_result(result: ToolResult, result_text: str): | ||
| if result.system: | ||
| result_text = f"<system>{result.system}</system>\n{result_text}" | ||
| return result_text | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.