From 69f4e612aa74d2ef916f1ead00bd28889d547871 Mon Sep 17 00:00:00 2001 From: Aman Rusia Date: Thu, 21 Nov 2024 15:00:04 +0530 Subject: [PATCH] Diff based file editing for all modes --- gpt_action_json_schema.json | 124 +++++++++++++++++------ gpt_instructions.txt | 53 +++++++++- pyproject.toml | 2 +- src/wcgw/client/anthropic_client.py | 20 ++-- src/wcgw/client/diff-instructions.txt | 44 +++++++++ src/wcgw/client/openai_client.py | 24 +++-- src/wcgw/client/tools.py | 136 ++++++++++++++++---------- src/wcgw/relay/serve.py | 73 ++++---------- src/wcgw/types_.py | 5 + uv.lock | 2 +- 10 files changed, 323 insertions(+), 160 deletions(-) create mode 100644 src/wcgw/client/diff-instructions.txt diff --git a/gpt_action_json_schema.json b/gpt_action_json_schema.json index 0c3804f..bfc3235 100644 --- a/gpt_action_json_schema.json +++ b/gpt_action_json_schema.json @@ -6,20 +6,20 @@ }, "servers": [ { - "url": "https://abe7-106-213-82-238.ngrok-free.app" + "url": "https://7d75-103-212-152-58.ngrok-free.app" } ], "paths": { - "/v1/write_file": { + "/v1/create_file": { "post": { "x-openai-isConsequential": false, - "summary": "Write File", - "operationId": "write_file_v1_write_file_post", + "summary": "Create File", + "operationId": "create_file_v1_create_file_post", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WritefileWithUUID" + "$ref": "#/components/schemas/CreateFileNewWithUUID" } } }, @@ -32,7 +32,47 @@ "application/json": { "schema": { "type": "string", - "title": "Response Write File V1 Write File Post" + "title": "Response Create File V1 Create File Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/full_file_edit": { + "post": { + "x-openai-isConsequential": false, + "summary": "File Edit Find Replace", + "operationId": "file_edit_find_replace_v1_full_file_edit_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullFileEditWithUUID" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response File Edit Find Replace V1 Full File Edit Post" } } } @@ -255,6 +295,54 @@ ], "title": "CommandWithUUID" }, + "CreateFileNewWithUUID": { + "properties": { + "file_path": { + "type": "string", + "title": "File Path" + }, + "file_content": { + "type": "string", + "title": "File Content" + }, + "user_id": { + "type": "string", + "format": "uuid", + "title": "User Id" + } + }, + "type": "object", + "required": [ + "file_path", + "file_content", + "user_id" + ], + "title": "CreateFileNewWithUUID" + }, + "FullFileEditWithUUID": { + "properties": { + "file_path": { + "type": "string", + "title": "File Path" + }, + "file_edit_using_searh_replace_blocks": { + "type": "string", + "title": "File Edit Using Searh Replace Blocks" + }, + "user_id": { + "type": "string", + "format": "uuid", + "title": "User Id" + } + }, + "type": "object", + "required": [ + "file_path", + "file_edit_using_searh_replace_blocks", + "user_id" + ], + "title": "FullFileEditWithUUID" + }, "HTTPValidationError": { "properties": { "detail": { @@ -323,30 +411,6 @@ "type" ], "title": "ValidationError" - }, - "WritefileWithUUID": { - "properties": { - "file_path": { - "type": "string", - "title": "File Path" - }, - "file_content": { - "type": "string", - "title": "File Content" - }, - "user_id": { - "type": "string", - "format": "uuid", - "title": "User Id" - } - }, - "type": "object", - "required": [ - "file_path", - "file_content", - "user_id" - ], - "title": "WritefileWithUUID" } } } diff --git a/gpt_instructions.txt b/gpt_instructions.txt index 6042cd0..32b13ff 100644 --- a/gpt_instructions.txt +++ b/gpt_instructions.txt @@ -15,12 +15,12 @@ Instructions for `BashCommand`: - Status of the command and the current working directory will always be returned at the end. - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands. - The first line might be `(...truncated)` if the output is too long. -- Always run `pwd` if you get any file or directory not found error to make sure you're not lost. -Instructions for `Write File` -- Write content to a file. Provide file path and content. Use this instead of BashCommand for writing files. +Instructions for `Create File New` +- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files. - This doesn't create any directories, please create directories using `mkdir -p` BashCommand. - Provide absolute file path only. +- For editing existing files, use FullFileEdit. Instructions for `BashInteraction` - Interact with running program using this tool @@ -31,7 +31,54 @@ Instructions for `BashInteraction` Instructions for `ResetShell` - Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly. +Instructions for `FullFileEdit`: + - Use absolute file path only. + - Use SEARCH/REPLACE blocks to edit the file. + Only edit the files using the following SEARCH/REPLACE blocks. + ``` + <<<<<<< SEARCH + ======= + def hello(): + "print a greeting" + + print("hello") + >>>>>>> REPLACE + + <<<<<<< SEARCH + def hello(): + "print a greeting" + + print("hello") + ======= + from hello import hello + >>>>>>> REPLACE + ``` + + # *SEARCH/REPLACE block* Rules: + + Every *SEARCH/REPLACE block* must use this format: + 1. The start of search block: <<<<<<< SEARCH + 2. A contiguous chunk of lines to search for in the existing source code + 3. The dividing line: ======= + 4. The lines to replace into the source code + 5. The end of the replace block: >>>>>>> REPLACE + + Use the *FULL* file path, as shown to you by the user. + + Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc. + If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup. + + *SEARCH/REPLACE* blocks will *only* replace the first match occurrence. + Including multiple unique *SEARCH/REPLACE* blocks if needed. + Include enough lines in each SEARCH section to uniquely match each set of lines that need to change. + + Keep *SEARCH/REPLACE* blocks concise. + Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file. + Include just the changing lines, and a few surrounding lines if needed for uniqueness. + Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks. + --- +Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd. Always critically think and debate with yourself to solve the problem. Understand the context and the code by reading as much resources as possible before writing a single piece of code. diff --git a/pyproject.toml b/pyproject.toml index 5cf1da7..8dd4e9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }] name = "wcgw" -version = "1.0.3" +version = "1.1.0" description = "What could go wrong giving full shell access to chatgpt?" readme = "README.md" requires-python = ">=3.10, <3.13" diff --git a/src/wcgw/client/anthropic_client.py b/src/wcgw/client/anthropic_client.py index 5555471..6ce164c 100644 --- a/src/wcgw/client/anthropic_client.py +++ b/src/wcgw/client/anthropic_client.py @@ -26,6 +26,7 @@ BashInteraction, CreateFileNew, FileEditFindReplace, + FullFileEdit, ReadImage, Writefile, ResetShell, @@ -143,8 +144,6 @@ def loop( history = json.load(f) if len(history) <= 2: raise ValueError("Invalid history file") - if history[1]["role"] != "user": - raise ValueError("Invalid history file, second message should be user") first_message = "" waiting_for_assistant = history[-1]["role"] != "assistant" @@ -185,7 +184,7 @@ def loop( - Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files. - This doesn't create any directories, please create directories using `mkdir -p` BashCommand. - Provide absolute file path only. -- For editing existing files, use FileEditFindReplace. +- For editing existing files, use FullFileEdit. """, ), ToolParam( @@ -199,12 +198,11 @@ def loop( description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.", ), ToolParam( - input_schema=FileEditFindReplace.model_json_schema(), - name="FileEditFindReplace", + input_schema=FullFileEdit.model_json_schema(), + name="FullFileEdit", description=""" -- Find and replace multiple lines in a file. - Use absolute file path only. -- Replaces complete lines. +- Use SEARCH/REPLACE blocks to edit the file. """, ), ] @@ -225,9 +223,10 @@ def loop( - Machine: {uname_machine} """ - if not history: - history = [{"role": "assistant", "content": system}] - else: + with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f: + system += f.read() + + if history: if ( (last_msg := history[-1])["role"] == "user" and isinstance((content := last_msg["content"]), dict) @@ -275,6 +274,7 @@ def loop( messages=history, tools=tools, max_tokens=8096, + system=system, ) system_console.print( diff --git a/src/wcgw/client/diff-instructions.txt b/src/wcgw/client/diff-instructions.txt new file mode 100644 index 0000000..52e2efd --- /dev/null +++ b/src/wcgw/client/diff-instructions.txt @@ -0,0 +1,44 @@ + +Instructions for +Only edit the files using the following SEARCH/REPLACE blocks. +``` +<<<<<<< SEARCH +======= +def hello(): + "print a greeting" + + print("hello") +>>>>>>> REPLACE + +<<<<<<< SEARCH +def hello(): + "print a greeting" + + print("hello") +======= +from hello import hello +>>>>>>> REPLACE +``` + +# *SEARCH/REPLACE block* Rules: + +Every *SEARCH/REPLACE block* must use this format: +1. The start of search block: <<<<<<< SEARCH +2. A contiguous chunk of lines to search for in the existing source code +3. The dividing line: ======= +4. The lines to replace into the source code +5. The end of the replace block: >>>>>>> REPLACE + +Use the *FULL* file path, as shown to you by the user. + +Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc. +If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup. + +*SEARCH/REPLACE* blocks will *only* replace the first match occurrence. +Including multiple unique *SEARCH/REPLACE* blocks if needed. +Include enough lines in each SEARCH section to uniquely match each set of lines that need to change. + +Keep *SEARCH/REPLACE* blocks concise. +Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file. +Include just the changing lines, and a few surrounding lines if needed for uniqueness. +Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks. \ No newline at end of file diff --git a/src/wcgw/client/openai_client.py b/src/wcgw/client/openai_client.py index 7c067e6..d249864 100644 --- a/src/wcgw/client/openai_client.py +++ b/src/wcgw/client/openai_client.py @@ -23,7 +23,8 @@ from ..types_ import ( BashCommand, BashInteraction, - FileEditFindReplace, + CreateFileNew, + FullFileEdit, ReadImage, Writefile, ResetShell, @@ -141,8 +142,6 @@ def loop( history = json.load(f) if len(history) <= 2: raise ValueError("Invalid history file") - if history[1]["role"] != "user": - raise ValueError("Invalid history file, second message should be user") first_message = "" waiting_for_assistant = history[-1]["role"] != "assistant" @@ -181,11 +180,20 @@ def loop( - Only one of send_text, send_specials, send_ascii should be provided.""", ), openai.pydantic_function_tool( - Writefile, + CreateFileNew, description=""" -- Write content to a file. Provide file path and content. Use this instead of BashCommand for writing files. +- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files. - This doesn't create any directories, please create directories using `mkdir -p` BashCommand. -- Provide absolute file path only.""", +- Provide absolute file path only. +- For editing existing files, use FullFileEdit.""", + ), + openai.pydantic_function_tool( + FullFileEdit, + description=""" +- Use absolute file path only. +- Use ONLY SEARCH/REPLACE blocks to edit the file. +- file_edit_using_searh_replace_blocks should start with <<<<<<< SEARCH +""", ), openai.pydantic_function_tool( ReadImage, description="Read an image from the shell." @@ -210,8 +218,12 @@ def loop( System information: - System: {uname_sysname} - Machine: {uname_machine} + """ + with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f: + system += f.read() + if not history: history = [{"role": "system", "content": system}] else: diff --git a/src/wcgw/client/tools.py b/src/wcgw/client/tools.py index 6b4339a..47f39ea 100644 --- a/src/wcgw/client/tools.py +++ b/src/wcgw/client/tools.py @@ -1,5 +1,6 @@ import asyncio import base64 +from concurrent.futures import ThreadPoolExecutor, as_completed from io import BytesIO import json import mimetypes @@ -48,9 +49,9 @@ from ..types_ import ( CreateFileNew, FileEditFindReplace, + FullFileEdit, ResetShell, Writefile, - FullFileDiff, ) from ..types_ import BashCommand @@ -479,7 +480,9 @@ def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str: closest_match, min_edit_distance = find_least_edit_distance_substring( content, find_lines ) - print(f"Exact match not found, found with edit distance: {min_edit_distance}") + print( + f"Exact match not found, found with whitespace removed edit distance: {min_edit_distance}" + ) if min_edit_distance / len(find_lines) < 1 / 100: print("Editing file with closest match") return edit_content(content, closest_match, replace_with_lines) @@ -491,6 +494,56 @@ def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str: return content +def do_diff_edit(fedit: FullFileEdit) -> str: + console.log(f"Editing file: {fedit.file_path}") + + if not os.path.isabs(fedit.file_path): + raise Exception("Failure: file_path should be absolute path") + else: + path_ = fedit.file_path + + if not os.path.exists(path_): + raise Exception(f"Error: file {path_} does not exist") + + with open(path_) as f: + apply_diff_to = f.read() + + lines = fedit.file_edit_using_searh_replace_blocks.split("\n") + n_lines = len(lines) + i = 0 + while i < n_lines: + if re.match(r"^<<<<<<+\s*SEARCH\s*$", lines[i]): + search_block = [] + i += 1 + while i < n_lines and not re.match(r"^======*\s*$", lines[i]): + search_block.append(lines[i]) + i += 1 + i += 1 + replace_block = [] + while i < n_lines and not re.match(r"^>>>>>>+\s*REPLACE\s*$", lines[i]): + replace_block.append(lines[i]) + i += 1 + i += 1 + + for line in search_block: + console.log("> " + line) + console.log("---") + for line in replace_block: + console.log("< " + line) + + search_block_ = "\n".join(search_block) + replace_block_ = "\n".join(replace_block) + + apply_diff_to = edit_content(apply_diff_to, search_block_, replace_block_) + else: + i += 1 + + with open(path_, "w") as f: + f.write(apply_diff_to) + + return "Success" + + def file_edit(fedit: FileEditFindReplace) -> str: if not os.path.isabs(fedit.file_path): raise Exception("Failure: file_path should be absolute path") @@ -550,6 +603,7 @@ def take_help_of_ai_assistant( | Writefile | CreateFileNew | FileEditFindReplace + | FullFileEdit | AIAssistant | DoneFlag | ReadImage @@ -576,6 +630,8 @@ def which_tool_name(name: str) -> Type[TOOLS]: return CreateFileNew elif name == "FileEditFindReplace": return FileEditFindReplace + elif name == "FullFileEdit": + return FullFileEdit elif name == "AIAssistant": return AIAssistant elif name == "DoneFlag": @@ -595,6 +651,7 @@ def get_tool_output( | Writefile | CreateFileNew | FileEditFindReplace + | FullFileEdit | AIAssistant | DoneFlag | ReadImage, @@ -612,6 +669,7 @@ def get_tool_output( | Writefile | CreateFileNew | FileEditFindReplace + | FullFileEdit | AIAssistant | DoneFlag | ReadImage @@ -623,6 +681,7 @@ def get_tool_output( | Writefile | CreateFileNew | FileEditFindReplace + | FullFileEdit | AIAssistant | DoneFlag | ReadImage @@ -646,6 +705,9 @@ def get_tool_output( elif isinstance(arg, FileEditFindReplace): console.print("Calling file edit tool") output = file_edit(arg), 0.0 + elif isinstance(arg, FullFileEdit): + console.print("Calling full file edit tool") + output = do_diff_edit(arg), 0.0 elif isinstance(arg, DoneFlag): console.print("Calling mark finish tool") output = mark_finish(arg), 0.0 @@ -681,44 +743,22 @@ class Mdata(BaseModel): | CreateFileNew | ResetShell | FileEditFindReplace + | FullFileEdit ) -execution_lock = threading.Lock() - - -def execute_user_input() -> None: - while True: - discard_input() - user_input = input() - with execution_lock: - try: - console.log( - execute_bash( - default_enc, - BashInteraction( - send_ascii=[ord(x) for x in user_input] + [ord("\n")] - ), - max_tokens=None, - )[0] - ) - except Exception as e: - traceback.print_exc() - console.log(f"Error: {e}") - - -async def register_client(server_url: str, client_uuid: str = "") -> None: +def register_client(server_url: str, client_uuid: str = "") -> None: global default_enc, default_model, curr_cost # Generate a unique UUID for this client if not client_uuid: client_uuid = str(uuid.uuid4()) # Create the WebSocket connection - async with websockets.connect(f"{server_url}/{client_uuid}") as websocket: - server_version = str(await websocket.recv()) + with syncconnect(f"{server_url}/{client_uuid}") as websocket: + server_version = str(websocket.recv()) print(f"Server version: {server_version}") client_version = importlib.metadata.version("wcgw") - await websocket.send(client_version) + websocket.send(client_version) print( f"Connected. Share this user id with the chatbot: {client_uuid} \nLink: https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access" @@ -726,24 +766,23 @@ async def register_client(server_url: str, client_uuid: str = "") -> None: try: while True: # Wait to receive data from the server - message = await websocket.recv() + message = websocket.recv() mdata = Mdata.model_validate_json(message) - with execution_lock: - try: - output, cost = get_tool_output( - mdata.data, default_enc, 0.0, lambda x, y: ("", 0), None - ) - curr_cost += cost - print(f"{curr_cost=}") - except Exception as e: - output = f"GOT EXCEPTION while calling tool. Error: {e}" - traceback.print_exc() - assert isinstance(output, str) - await websocket.send(output) + try: + output, cost = get_tool_output( + mdata.data, default_enc, 0.0, lambda x, y: ("", 0), None + ) + curr_cost += cost + print(f"{curr_cost=}") + except Exception as e: + output = f"GOT EXCEPTION while calling tool. Error: {e}" + traceback.print_exc() + assert isinstance(output, str) + websocket.send(output) except (websockets.ConnectionClosed, ConnectionError): print(f"Connection closed for UUID: {client_uuid}, retrying") - await register_client(server_url, client_uuid) + register_client(server_url, client_uuid) run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True) @@ -760,13 +799,4 @@ def app( print(f"wcgw version: {version_}") exit() - thread1 = threading.Thread(target=execute_user_input) - thread2 = threading.Thread( - target=asyncio.run, args=(register_client(server_url, client_uuid or ""),) - ) - - thread1.start() - thread2.start() - - thread1.join() - thread2.join() + register_client(server_url, client_uuid or "") diff --git a/src/wcgw/relay/serve.py b/src/wcgw/relay/serve.py index b9f3904..4478e45 100644 --- a/src/wcgw/relay/serve.py +++ b/src/wcgw/relay/serve.py @@ -17,7 +17,9 @@ from ..types_ import ( BashCommand, BashInteraction, + CreateFileNew, FileEditFindReplace, + FullFileEdit, ResetShell, Writefile, Specials, @@ -25,7 +27,15 @@ class Mdata(BaseModel): - data: BashCommand | BashInteraction | Writefile | ResetShell | FileEditFindReplace + data: ( + BashCommand + | BashInteraction + | Writefile + | CreateFileNew + | ResetShell + | FileEditFindReplace + | FullFileEdit + ) user_id: UUID @@ -38,39 +48,6 @@ class Mdata(BaseModel): images: DefaultDict[UUID, dict[str, dict[str, Any]]] = DefaultDict(dict) -@app.websocket("/register_serve_image/{uuid}") -async def register_serve_image(websocket: WebSocket, uuid: UUID) -> None: - raise Exception("Disabled") - await websocket.accept() - received_data = await websocket.receive_json() - name = received_data["name"] - image_b64 = received_data["image_b64"] - image_bytes = base64.b64decode(image_b64) - images[uuid][name] = { - "content": image_bytes, - "media_type": received_data["media_type"], - } - - -@app.get("/get_image/{uuid}/{name}") -async def get_image(uuid: UUID, name: str) -> fastapi.responses.Response: - return fastapi.responses.Response( - content=images[uuid][name]["content"], - media_type=images[uuid][name]["media_type"], - ) - - -@app.websocket("/register/{uuid}") -async def register_websocket_deprecated(websocket: WebSocket, uuid: UUID) -> None: - await websocket.accept() - await websocket.send_text( - "Outdated client used. Deprecated api is being used. Upgrade the wcgw app." - ) - await websocket.close( - reason="This endpoint is deprecated. Please use /v1/register/{uuid}", code=1002 - ) - - CLIENT_VERSION_MINIMUM = "1.0.0" @@ -116,20 +93,12 @@ async def send_data_callback(data: Mdata) -> None: print(f"Client {uuid} disconnected") -@app.post("/write_file") -async def write_file_deprecated(write_file_data: Writefile, user_id: UUID) -> Response: - return Response( - content="This version of the API is deprecated. Please upgrade your client.", - status_code=400, - ) - - -class WritefileWithUUID(Writefile): +class CreateFileNewWithUUID(CreateFileNew): user_id: UUID -@app.post("/v1/write_file") -async def write_file(write_file_data: WritefileWithUUID) -> str: +@app.post("/v1/create_file") +async def create_file(write_file_data: CreateFileNewWithUUID) -> str: user_id = write_file_data.user_id if user_id not in clients: return "Failure: id not found, ask the user to check it." @@ -153,13 +122,13 @@ def put_results(result: str) -> None: raise fastapi.HTTPException(status_code=500, detail="Timeout error") -class FileEditFindReplaceWithUUID(FileEditFindReplace): +class FullFileEditWithUUID(FullFileEdit): user_id: UUID -@app.post("/v1/file_edit_find_replace") +@app.post("/v1/full_file_edit") async def file_edit_find_replace( - file_edit_find_replace: FileEditFindReplaceWithUUID, + file_edit_find_replace: FullFileEditWithUUID, ) -> str: user_id = file_edit_find_replace.user_id if user_id not in clients: @@ -218,14 +187,6 @@ def put_results(result: str) -> None: raise fastapi.HTTPException(status_code=500, detail="Timeout error") -@app.post("/execute_bash") -async def execute_bash_deprecated(excute_bash_data: Any, user_id: UUID) -> Response: - return Response( - content="This version of the API is deprecated. Please upgrade your client.", - status_code=400, - ) - - class CommandWithUUID(BaseModel): command: str user_id: UUID diff --git a/src/wcgw/types_.py b/src/wcgw/types_.py index adebb19..026c96b 100644 --- a/src/wcgw/types_.py +++ b/src/wcgw/types_.py @@ -56,3 +56,8 @@ class FileEditFindReplace(BaseModel): class ResetShell(BaseModel): should_reset: Literal[True] = True + + +class FullFileEdit(BaseModel): + file_path: str + file_edit_using_searh_replace_blocks: str diff --git a/uv.lock b/uv.lock index 9b42e68..0a9666b 100644 --- a/uv.lock +++ b/uv.lock @@ -948,7 +948,7 @@ wheels = [ [[package]] name = "wcgw" -version = "1.0.3" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "anthropic" },