Files
simular-ai--agent-s/gui_agents/s1/utils/common_utils.py
T
wehub-resource-sync c8c954c85d
lint / build (3.10) (push) Failing after 1s
lint / build (3.11) (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:23:35 +08:00

864 lines
28 KiB
Python

import base64
import io
import json
import os
import pickle
import re
import tempfile
import time
import xml.etree.ElementTree as ET
from io import BytesIO
from typing import Dict, List, Tuple, Union
from xml.etree.ElementTree import Element
import numpy as np
import tiktoken
from PIL import Image, ImageDraw, ImageFont
from pydantic import BaseModel, ValidationError
def find_leaf_nodes(xlm_file_str):
if not xlm_file_str:
return []
root = ET.fromstring(xlm_file_str)
# Recursive function to traverse the XML tree and collect leaf nodes
def collect_leaf_nodes(node, leaf_nodes):
# If the node has no children, it is a leaf node, add it to the list
if not list(node):
leaf_nodes.append(node)
# If the node has children, recurse on each child
for child in node:
collect_leaf_nodes(child, leaf_nodes)
# List to hold all leaf nodes
leaf_nodes = []
collect_leaf_nodes(root, leaf_nodes)
return leaf_nodes
state_ns = "uri:deskat:state.at-spi.gnome.org"
component_ns = "uri:deskat:component.at-spi.gnome.org"
class Node(BaseModel):
name: str
info: str
class Dag(BaseModel):
nodes: List[Node]
edges: List[List[Node]]
NUM_IMAGE_TOKEN = 1105 # Value set of screen of size 1920x1080 for openai vision
def call_llm_safe(agent) -> Union[str, Dag]:
# Retry if fails
max_retries = 3 # Set the maximum number of retries
attempt = 0
response = ""
while attempt < max_retries:
try:
response = agent.get_response()
break # If successful, break out of the loop
except Exception as e:
attempt += 1
print(f"Attempt {attempt} failed: {e}")
if attempt == max_retries:
print("Max retries reached. Handling failure.")
time.sleep(1.0)
return response
def calculate_tokens(messages, num_image_token=NUM_IMAGE_TOKEN) -> Tuple[int, int]:
num_input_images = 0
output_message = messages[-1]
input_message = messages[:-1]
input_string = """"""
for message in input_message:
input_string += message["content"][0]["text"] + "\n"
if len(message["content"]) > 1:
num_input_images += 1
input_text_tokens = get_input_token_length(input_string)
input_image_tokens = num_image_token * num_input_images
output_tokens = get_input_token_length(output_message["content"][0]["text"])
return (input_text_tokens + input_image_tokens), output_tokens
def judge_node(node: Element, platform="ubuntu", check_image=False) -> bool:
keeps: bool = (
node.tag.startswith("document")
or node.tag.endswith("item")
or node.tag.endswith("button")
or node.tag.endswith("heading")
or node.tag.endswith("label")
or node.tag.endswith("scrollbar")
or node.tag.endswith("searchbox")
or node.tag.endswith("textbox")
or node.tag.endswith("link")
or node.tag.endswith("tabelement")
or node.tag.endswith("textfield")
or node.tag.endswith("textarea")
or node.tag.endswith("menu")
or node.tag.endswith("menu-item")
or node.tag
in {
"alert",
"canvas",
"check-box",
"combo-box",
"entry",
"icon",
"image",
"paragraph",
"scroll-bar",
"section",
"slider",
"static",
"table-cell",
"terminal",
"text",
"netuiribbontab",
"start",
"trayclockwclass",
"traydummysearchcontrol",
"uiimage",
"uiproperty",
"uiribboncommandbar",
}
)
keeps = (
keeps
and (
platform == "ubuntu"
and node.get("{{{:}}}showing".format(state_ns), "false") == "true"
and node.get("{{{:}}}visible".format(state_ns), "false") == "true"
or platform == "windows"
and node.get("{{{:}}}visible".format(state_ns), "false") == "true"
)
and (
node.get("name", "") != ""
or node.text is not None
and len(node.text) > 0
or check_image
and node.get("image", "false") == "true"
)
)
# and (node.get("{{{:}}}enabled".format(state_ns), "false") == "true" \
# or node.get("{{{:}}}editable".format(state_ns), "false") == "true" \
# or node.get("{{{:}}}expandable".format(state_ns), "false") == "true" \
# or node.get("{{{:}}}checkable".format(state_ns), "false") == "true"
# ) \
coordinates: Tuple[int, int] = eval(
node.get("{{{:}}}screencoord".format(component_ns), "(-1, -1)")
)
sizes: Tuple[int, int] = eval(
node.get("{{{:}}}size".format(component_ns), "(-1, -1)")
)
keeps = (
keeps
and coordinates[0] >= 0
and coordinates[1] >= 0
and sizes[0] > 0
and sizes[1] > 0
)
return keeps
def filter_nodes(root: Element, platform="ubuntu", check_image=False):
filtered_nodes = []
all_nodes = []
for node in root.iter():
all_nodes.append(node)
for node in root.iter():
if judge_node(node, platform, check_image):
filtered_nodes.append(node)
return filtered_nodes
def draw_bounding_boxes(nodes, image_file_content, down_sampling_ratio=1.0):
# Load the screenshot image
image_stream = io.BytesIO(image_file_content)
image = Image.open(image_stream)
if float(down_sampling_ratio) != 1.0:
image = image.resize(
(
int(image.size[0] * down_sampling_ratio),
int(image.size[1] * down_sampling_ratio),
)
)
draw = ImageDraw.Draw(image)
marks = []
drew_nodes = []
text_informations: List[str] = ["index\ttag\tname\ttext"]
try:
# Adjust the path to the font file you have or use a default one
font = ImageFont.truetype("arial.ttf", 15)
except IOError:
# Fallback to a basic font if the specified font can't be loaded
font = ImageFont.load_default()
index = 1
# Loop over all the visible nodes and draw their bounding boxes
for _node in nodes:
coords_str = _node.attrib.get(
"{uri:deskat:component.at-spi.gnome.org}screencoord"
)
size_str = _node.attrib.get("{uri:deskat:component.at-spi.gnome.org}size")
if coords_str and size_str:
try:
# Parse the coordinates and size from the strings
coords = tuple(map(int, coords_str.strip("()").split(", ")))
size = tuple(map(int, size_str.strip("()").split(", ")))
import copy
original_coords = copy.deepcopy(coords)
original_size = copy.deepcopy(size)
if float(down_sampling_ratio) != 1.0:
# Downsample the coordinates and size
coords = tuple(int(coord * down_sampling_ratio) for coord in coords)
size = tuple(int(s * down_sampling_ratio) for s in size)
# Check for negative sizes
if size[0] <= 0 or size[1] <= 0:
raise ValueError(f"Size must be positive, got: {size}")
# Calculate the bottom-right corner of the bounding box
bottom_right = (coords[0] + size[0], coords[1] + size[1])
# Check that bottom_right > coords (x1 >= x0, y1 >= y0)
if bottom_right[0] < coords[0] or bottom_right[1] < coords[1]:
raise ValueError(
f"Invalid coordinates or size, coords: {coords}, size: {size}"
)
# Check if the area only contains one color
cropped_image = image.crop((*coords, *bottom_right))
if len(set(list(cropped_image.getdata()))) == 1:
continue
# Draw rectangle on image
draw.rectangle([coords, bottom_right], outline="red", width=1)
# Draw index number at the bottom left of the bounding box with black background
text_position = (
coords[0],
bottom_right[1],
) # Adjust Y to be above the bottom right
text_bbox: Tuple[int, int, int, int] = draw.textbbox(
text_position, str(index), font=font, anchor="lb"
)
# offset: int = bottom_right[1]-text_bbox[3]
# text_bbox = (text_bbox[0], text_bbox[1]+offset, text_bbox[2], text_bbox[3]+offset)
# draw.rectangle([text_position, (text_position[0] + 25, text_position[1] + 18)], fill='black')
draw.rectangle(text_bbox, fill="black")
draw.text(
text_position, str(index), font=font, anchor="lb", fill="white"
)
# each mark is an x, y, w, h tuple
marks.append(
[
original_coords[0],
original_coords[1],
original_size[0],
original_size[1],
]
)
drew_nodes.append(_node)
if _node.text:
node_text = (
_node.text
if '"' not in _node.text
else '"{:}"'.format(_node.text.replace('"', '""'))
)
elif _node.get(
"{uri:deskat:uia.windows.microsoft.org}class", ""
).endswith("EditWrapper") and _node.get(
"{uri:deskat:value.at-spi.gnome.org}value"
):
node_text: str = _node.get(
"{uri:deskat:value.at-spi.gnome.org}value"
)
node_text = (
node_text
if '"' not in node_text
else '"{:}"'.format(node_text.replace('"', '""'))
)
else:
node_text = '""'
text_information: str = "{:d}\t{:}\t{:}\t{:}".format(
index, _node.tag, _node.get("name", ""), node_text
)
text_informations.append(text_information)
index += 1
except ValueError:
pass
output_image_stream = io.BytesIO()
image.save(output_image_stream, format="PNG")
image_content = output_image_stream.getvalue()
return marks, drew_nodes, "\n".join(text_informations), image_content
def print_nodes_with_indent(nodes, indent=0):
for node in nodes:
print(" " * indent, node.tag, node.attrib)
print_nodes_with_indent(node, indent + 2)
# Code based on https://github.com/xlang-ai/OSWorld/blob/main/mm_agents/agent.py
def encode_image(image_content):
return base64.b64encode(image_content).decode("utf-8")
def encoded_img_to_pil_img(data_str):
base64_str = data_str.replace("data:image/png;base64,", "")
image_data = base64.b64decode(base64_str)
image = Image.open(BytesIO(image_data))
return image
def save_to_tmp_img_file(data_str):
base64_str = data_str.replace("data:image/png;base64,", "")
image_data = base64.b64decode(base64_str)
image = Image.open(BytesIO(image_data))
tmp_img_path = os.path.join(tempfile.mkdtemp(), "tmp_img.png")
image.save(tmp_img_path)
return tmp_img_path
def linearize_accessibility_tree(accessibility_tree, platform="ubuntu", tag=False):
# leaf_nodes = find_leaf_nodes(accessibility_tree)
filtered_nodes = filter_nodes(ET.fromstring(accessibility_tree), platform)
linearized_accessibility_tree = [
"tag\tname\ttext\tposition (top-left x&y)\tsize (w&h)"
]
# Linearize the accessibility tree nodes into a table format
for node in filtered_nodes:
# linearized_accessibility_tree += node.tag + "\t"
# linearized_accessibility_tree += node.attrib.get('name') + "\t"
if node.text:
text = (
node.text
if '"' not in node.text
else '"{:}"'.format(node.text.replace('"', '""'))
)
elif node.get("{uri:deskat:uia.windows.microsoft.org}class", "").endswith(
"EditWrapper"
) and node.get("{uri:deskat:value.at-spi.gnome.org}value"):
text: str = node.get("{uri:deskat:value.at-spi.gnome.org}value")
text = text if '"' not in text else '"{:}"'.format(text.replace('"', '""'))
else:
text = '""'
# linearized_accessibility_tree += node.attrib.get(
# , "") + "\t"
# linearized_accessibility_tree += node.attrib.get('{uri:deskat:component.at-spi.gnome.org}size', "") + "\n"
linearized_accessibility_tree.append(
"{:}\t{:}\t{:}\t{:}\t{:}".format(
node.tag,
node.get("name", ""),
text,
node.get("{uri:deskat:component.at-spi.gnome.org}screencoord", ""),
node.get("{uri:deskat:component.at-spi.gnome.org}size", ""),
)
)
if tag:
linearized_accessibility_tree = tag_accessibility_tree(
linearized_accessibility_tree
)
return "\n".join(linearized_accessibility_tree)
def tag_accessibility_tree(linear_accessibility_tree):
# Add 'id' to the first line
linear_accessibility_tree[0] = "id\t" + linear_accessibility_tree[0]
# Start idx from 1 to correctly index into the list
for idx in range(1, len(linear_accessibility_tree)):
line = linear_accessibility_tree[idx]
linear_accessibility_tree[idx] = f"[{str(idx)}]\t" + line
return linear_accessibility_tree
def tag_screenshot(screenshot, accessibility_tree, platform="ubuntu"):
nodes = filter_nodes(
ET.fromstring(accessibility_tree), platform=platform, check_image=True
)
# Make tag screenshot
marks, drew_nodes, element_list, tagged_screenshot = draw_bounding_boxes(
nodes, screenshot
)
return marks, drew_nodes, tagged_screenshot, element_list
def parse_dag(text):
pattern = r"<json>(.*?)</json>"
match = re.search(pattern, text, re.DOTALL)
if match:
json_str = match.group(1)
try:
json_data = json.loads(json_str)
return Dag(**json_data["dag"])
except json.JSONDecodeError:
print("Error: Invalid JSON")
return None
except KeyError:
print("Error: 'dag' key not found in JSON")
return None
except ValidationError as e:
print(f"Error: Invalid data structure - {e}")
return None
else:
print("Error: JSON not found")
return None
def parse_subinfo(subinfo_string):
matches = re.findall(r"```json\s+(.*?)\s+```", subinfo_string, re.DOTALL)
if matches:
# Assuming there's only one match, parse the JSON string into a dictionary
try:
subinfo_dict = json.loads(matches[0])
return subinfo_dict
except json.JSONDecodeError as e:
print(f"Failed to parse JSON: {e}")
return {"error": e}
else:
return {
"error": "Subinfo generated in incorrect format. Please use the correct format."
}
def parse_actions_from_string(input_string):
if input_string.strip() in ["WAIT", "DONE", "FAIL"]:
return [input_string.strip()]
# Search for a JSON string within the input string
actions = []
matches = re.findall(r"```json\s+(.*?)\s+```", input_string, re.DOTALL)
if matches:
# Assuming there's only one match, parse the JSON string into a dictionary
try:
for match in matches:
action_dict = json.loads(match)
actions.append(action_dict)
return actions
except json.JSONDecodeError as e:
return f"Failed to parse JSON: {e}"
else:
matches = re.findall(r"```\s+(.*?)\s+```", input_string, re.DOTALL)
if matches:
# Assuming there's only one match, parse the JSON string into a dictionary
try:
for match in matches:
action_dict = json.loads(match)
actions.append(action_dict)
return actions
except json.JSONDecodeError as e:
return f"Failed to parse JSON: {e}"
else:
try:
action_dict = json.loads(input_string)
return [action_dict]
except json.JSONDecodeError:
raise ValueError("Invalid response format: " + input_string)
def parse_fixed_action_from_string(input_string):
pattern = r"```(?:\w+\s+)?(.*?)```"
matches = re.findall(pattern, input_string)
if matches:
# Assuming there's only one match, parse the JSON string into a dictionary
try:
for match in matches:
action = match
return action
except json.JSONDecodeError as e:
return f"Failed to parse JSON: {e}"
return "agent.wait()"
def parse_code_from_string(input_string):
input_string = "\n".join(
[line.strip() for line in input_string.split(";") if line.strip()]
)
if input_string.strip() in ["WAIT", "DONE", "FAIL"]:
return [input_string.strip()]
# This regular expression will match both ```code``` and ```python code```
# and capture the `code` part. It uses a non-greedy match for the content inside.
pattern = r"```(?:\w+\s+)?(.*?)```"
# Find all non-overlapping matches in the string
matches = re.findall(pattern, input_string, re.DOTALL)
# The regex above captures the content inside the triple backticks.
# The `re.DOTALL` flag allows the dot `.` to match newline characters as well,
# so the code inside backticks can span multiple lines.
# matches now contains all the captured code snippets
codes = []
for match in matches:
match = match.strip()
commands = [
"WAIT",
"DONE",
"FAIL",
] # fixme: updates this part when we have more commands
if match in commands:
codes.append(match.strip())
elif match.split("\n")[-1] in commands:
if len(match.split("\n")) > 1:
codes.append("\n".join(match.split("\n")[:-1]))
codes.append(match.split("\n")[-1])
else:
codes.append(match)
return codes
def parse_single_code_from_string(input_string):
input_string = input_string.strip()
if input_string.strip() in ["WAIT", "DONE", "FAIL"]:
return input_string.strip()
# This regular expression will match both ```code``` and ```python code```
# and capture the `code` part. It uses a non-greedy match for the content inside.
pattern = r"```(?:\w+\s+)?(.*?)```"
# Find all non-overlapping matches in the string
matches = re.findall(pattern, input_string, re.DOTALL)
# The regex above captures the content inside the triple backticks.
# The `re.DOTALL` flag allows the dot `.` to match newline characters as well,
# so the code inside backticks can span multiple lines.
# matches now contains all the captured code snippets
codes = []
for match in matches:
match = match.strip()
commands = [
"WAIT",
"DONE",
"FAIL",
] # fixme: updates this part when we have more commands
if match in commands:
codes.append(match.strip())
elif match.split("\n")[-1] in commands:
if len(match.split("\n")) > 1:
codes.append("\n".join(match.split("\n")[:-1]))
codes.append(match.split("\n")[-1])
else:
codes.append(match)
return codes[0]
def parse_action_from_fixed_code(action_string, linearized_accessibility_tree):
import re
def parse_action_from_agent_code(action_str):
# First, extract the code block within triple backticks
code_block_pattern = r"```(.*?)```"
code_block_match = re.search(code_block_pattern, action_str, re.DOTALL)
if not code_block_match:
raise ValueError("No code block found")
code_block = code_block_match.group(1).strip()
# Define a regex pattern to extract the action type and parameters
action_pattern = r"agent\.(\w+)\((.*?)\)"
match = re.match(action_pattern, code_block, re.IGNORECASE)
if match:
action_type = match.group(1)
params_str = match.group(2)
# Split the parameters by comma and strip any surrounding whitespace or quotes
params = [
param.strip().strip('"').strip("'") for param in params_str.split(",")
]
# Convert numeric parameters to integers
for i in range(len(params)):
try:
params[i] = int(params[i])
except ValueError:
pass
return action_type, params
else:
raise ValueError("Invalid action string format")
parsed_action = parse_action_from_agent_code(action_string)
action_type, params = parsed_action
code = ""
def get_position_from_tree(element_id):
element = linearized_accessibility_tree[element_id]
position_str, size_str = element.split("\t")[-2].replace("(", "").replace(
")", ""
), element.split("\t")[-1].replace("(", "").replace(")", "")
top_x_str, top_y_str = position_str.split(",")
top_x, top_y = int(top_x_str.strip()), int(top_y_str.strip())
size_x_str, size_y_str = size_str.split(",")
size_x, size_y = int(size_x_str.strip()), int(size_y_str.strip())
centroid_x, centroid_y = top_x + size_x // 2, top_y + size_y // 2
return centroid_x, centroid_y
if action_type == "left_click_element_by_id":
element_id = int(params[0])
centroid_x, centroid_y = get_position_from_tree(element_id)
code = f"""position = ({centroid_x}, {centroid_y}); pyautogui.click(position)
"""
elif action_type == "right_click_element_by_id":
element_id = int(params[0])
centroid_x, centroid_y = get_position_from_tree(element_id)
code = f"""
position = ({centroid_x}, {centroid_y}); pyautogui.click(position, button='right')
"""
elif action_type == "hover_over_element_by_id":
element_id = int(params[0])
centroid_x, centroid_y = get_position_from_tree(element_id)
code = (
f"""position = ({centroid_x}, {centroid_y}); pyautogui.moveTo(position)"""
)
elif action_type == "type_write_element_by_id":
element_id = int(params[0])
text = params[1]
centroid_x, centroid_y = get_position_from_tree(element_id)
code = f"""
position = ({centroid_x}, {centroid_y}); pyautogui.click(position); time.sleep(0.75); pyautogui.typewrite("{text}")"""
elif action_type == "press_key_combinations":
keys = params
keys_str = '", "'.join(keys)
code = f"""
pyautogui.hotkey("{keys_str}")
"""
elif action_type == "wait":
code = """WAIT"""
elif action_type == "done":
code = """DONE"""
elif action_type == "fail":
code = "FAIL"
return [code.strip()]
def parse_code_from_som_string(input_string, masks):
# parse the output string by masks
tag_vars = ""
for i, mask in enumerate(masks):
x, y, w, h = mask
tag_vars += (
"tag_"
+ str(i + 1)
+ "="
+ "({}, {})".format(int(x + w // 2), int(y + h // 2))
)
tag_vars += "\n"
actions = parse_code_from_string(input_string)
for i, action in enumerate(actions):
if action.strip() in ["WAIT", "DONE", "FAIL"]:
pass
else:
action = tag_vars + action
actions[i] = action
return actions
def box_iou(boxes1: np.ndarray, boxes2: np.ndarray) -> np.ndarray:
"""
Fast vectorized IOU implementation using only NumPy
boxes1: [N, 4] array of boxes
boxes2: [M, 4] array of boxes
Returns: [N, M] array of IOU values
"""
# Calculate areas of boxes1
area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
# Calculate areas of boxes2
area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
# Get intersections using broadcasting
lt = np.maximum(boxes1[:, None, :2], boxes2[None, :, :2]) # [N,M,2]
rb = np.minimum(boxes1[:, None, 2:], boxes2[None, :, 2:]) # [N,M,2]
# Calculate intersection areas
wh = np.clip(rb - lt, 0, None) # [N,M,2]
intersection = wh[:, :, 0] * wh[:, :, 1] # [N,M]
# Calculate union areas
union = area1[:, None] + area2[None, :] - intersection
# Calculate IOU
iou = np.where(union > 0, intersection / union, 0)
return iou
def calculate_iou(rect1, rect2):
"""
Calculate the Intersection over Union (IoU) of two rectangles using numpy.
Parameters:
rect1, rect2: Tuples containing the coordinates of the rectangles in the form (x_min, y_min, x_max, y_max)
Returns:
IoU: Intersection over Union value
"""
# Convert the coordinates to tensors
box1 = np.array([rect1], dtype=np.float32)
box2 = np.array([rect2], dtype=np.float32)
# Calculate IoU using numpy
iou = box_iou(box1, box2)
return iou
def text_cvt_orc_format_paddle(paddle_result):
texts = []
print("paddle_result: ", paddle_result)
for i, line in enumerate(paddle_result[0]):
points = np.array(line[0])
print("points: ", points)
location = {
"left": int(min(points[:, 0])),
"top": int(min(points[:, 1])),
"right": int(max(points[:, 0])),
"bottom": int(max(points[:, 1])),
}
print("location: ", location)
content = line[1][0]
texts.append((i, content, location))
return texts
def trim_accessibility_tree(linearized_accessibility_tree, max_tokens):
enc = tiktoken.encoding_for_model("gpt-4")
tokens = enc.encode(linearized_accessibility_tree)
if len(tokens) > max_tokens:
print("MAX TOKEN LENGTH OF ACCESSIBILITY TREE EXCEEDED")
linearized_accessibility_tree = enc.decode(tokens[:max_tokens])
linearized_accessibility_tree += "[...]\n"
return linearized_accessibility_tree
def get_input_token_length(input_string):
enc = tiktoken.encoding_for_model("gpt-4")
tokens = enc.encode(input_string)
return len(tokens)
def load_osworld_example(base_path: str, domain: str, id: int):
example_path = f"{base_path}/{domain}"
example_path = (
f"/Users/saaketagashe/Documents/OSWorld/evaluation_examples/examples/{domain}"
)
examples = os.listdir(example_path)
with open(example_path + "/" + examples[id], "r") as f:
example = json.load(f)
return example
def sanitize_code(code):
# This pattern captures the outermost double-quoted text
if "\n" in code:
pattern = r'(".*?")'
# Find all matches in the text
matches = re.findall(pattern, code, flags=re.DOTALL)
if matches:
# Replace the first occurrence only
first_match = matches[0]
code = code.replace(first_match, f'"""{first_match[1:-1]}"""', 1)
return code
def extract_first_agent_function(code_string):
# Regular expression pattern to match 'agent' functions with any arguments, including nested parentheses
pattern = r'agent\.[a-zA-Z_]+\((?:[^()\'"]|\'[^\']*\'|"[^"]*")*\)'
# Find all matches in the string
matches = re.findall(pattern, code_string)
# Return the first match if found, otherwise return None
return matches[0] if matches else None
def load_knowledge_base(kb_path: str) -> Dict:
try:
with open(kb_path, "r") as f:
return json.load(f)
except Exception as e:
print(f"Error loading knowledge base: {e}")
return {}
def load_embeddings(embeddings_path: str) -> Dict:
try:
with open(embeddings_path, "rb") as f:
return pickle.load(f)
except Exception as e:
print(f"Error loading embeddings: {e}")
return {}
def save_embeddings(embeddings_path: str, embeddings: Dict):
try:
with open(embeddings_path, "wb") as f:
pickle.dump(embeddings, f)
except Exception as e:
print(f"Error saving embeddings: {e}")