Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 28 additions & 7 deletions evoagentx/core/base_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# from pydantic import BaseModel
from typing import Optional, List
from typing import List, Optional

from jsonschema import Draft7Validator
from pydantic import model_validator

from .module import BaseModule


class BaseConfig(BaseModule):

"""
Expand Down Expand Up @@ -53,15 +57,32 @@ def get_set_params(self, ignore: List[str] = []) -> dict:

class Parameter(BaseModule):
"""Parameter class used to define configuration parameters.

Attributes:
name: Parameter name
type: Parameter type
type: Parameter type, support json & python type. if type is `object` or `array`, then schema is required.
description: Parameter description
required: Whether the parameter is required, defaults to True
json_schema: the json schema of the parameter, required when type is `object` or `array`.
"""
name: str
type: str
description: str
required: Optional[bool] = True
type: str
description: str
required: Optional[bool] = True
json_schema: Optional[dict] = None

@model_validator(mode="after")
def _validate_type_and_schema(self):
from ..utils.utils import string_to_python_type
if self.type not in string_to_python_type:
raise ValueError(f"Invalid `type`: {self.type}. Allowed: {list(string_to_python_type.keys())}")
if self.type in {"object", "array"} and not self.json_schema:
raise ValueError("`json_schema` is required when `type` is `object` or `array`.")
if self.json_schema is not None:
try:
Draft7Validator.check_schema(self.json_schema)
except Exception as e:
raise ValueError(f"Invalid `json_schema` for '{self.name}': {self.json_schema}.") from e
assert self.type == self.json_schema.get("type"), f"`type` and `json_schema.type` must be the same if `json_schema` is provided. But got `type`: {self.type}, `json_schema.type`: {self.json_schema.get('type')}"
return self

61 changes: 61 additions & 0 deletions evoagentx/core/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Any

from .module import BaseModule


class Metadata(BaseModule):

def __add__(self, other: "Metadata") -> "Metadata":
if not isinstance(other, Metadata):
raise TypeError(f"Cannot add {type(other)} to `Metadata`")

def merge_values(v1: Any, v2: Any, field_name: str) -> Any:
if isinstance(v1, (int, float)) and isinstance(v2, (int, float)):
return v1 + v2

elif isinstance(v1, list) and isinstance(v2, list):
return v1 + v2

elif isinstance(v1, set) and isinstance(v2, set):
return v1.union(v2)

elif isinstance(v1, dict) and isinstance(v2, dict):
merged = dict(v1)
for key, val2 in v2.items():
if key in merged:
merged[key] = merge_values(merged[key], val2, key)
else:
merged[key] = val2
return merged

elif isinstance(v1, str) and isinstance(v2, str):
if v1 == v2:
return v1
else:
raise ValueError(f"Cannot add strings '{v1}' and '{v2}' for field '{field_name}'")

elif isinstance(v1, bool) and isinstance(v2, bool):
return v1 or v2

elif isinstance(v1, Metadata) and isinstance(v2, Metadata):
return v1 + v2

else:
raise TypeError(f"Cannot add values of types {type(v1)} and {type(v2)} for field '{field_name}'")

merged_data = self.model_dump()
merged_data.pop("class_name", None)
other_data = other.model_dump()
other_data.pop("class_name", None)

for key, val2 in other_data.items():
if key in merged_data:
merged_data[key] = merge_values(merged_data[key], val2, key)
else:
merged_data[key] = val2

if type(self) is type(other):
return self.__class__(**merged_data)
else:
return Metadata(**merged_data)

93 changes: 44 additions & 49 deletions evoagentx/core/module_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import json
import os
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin
from types import UnionType
from typing import Any, Dict, List, Type, Union, get_args, get_origin
from uuid import uuid4

import regex
Expand Down Expand Up @@ -84,29 +85,6 @@ def save_json(data, path: str, type: str="json", use_indent: bool=True) -> str:
return path


def extract_fenced_blocks(text: str, labels: Optional[List[str]] = None) -> List[str]:
"""
Extract fenced code blocks from the given text.

Args:
text (str): The text to extract fenced code blocks from.
labels (List[str]): The labels to extract fenced code blocks for.

Returns:
List[str]: Code blocks with specified labels.
"""
# Pattern to match fenced blocks: ```label\ncode\n```
pattern = r"```([a-zA-Z0-9_\-\+]*)\s*\n*(.*?)\n*```"
matches = regex.findall(pattern, text, regex.DOTALL)

if labels:
# Normalize labels for case-insensitive matching
labels_lower = {label.lower() for label in labels}
return [code.strip() for lang, code in matches if lang.strip().lower() in labels_lower]

return [code.strip() for _, code in matches]


def escape_json_values(string: str) -> str:

def escape_value(match):
Expand Down Expand Up @@ -196,30 +174,25 @@ def _replacer(match) -> str:

def fix_json(string: str) -> str:
string = remove_json_comments(string)
string = fix_json_booleans(string)
# string = fix_json_booleans(string)
string = escape_json_values(string)
return string


def parse_json_from_text(text: str) -> List[str]:
"""
Autoregressively extract JSON object from text
Autoregressively extract JSON object from text

Args:
text (str): a text that includes JSON data

Args:
text (str): a text that includes JSON data

Returns:
List[str]: a list of parsed JSON data
"""
fenced_blocks = extract_fenced_blocks(text)
if fenced_blocks:
matches = fenced_blocks
else:
json_pattern = r"""(?:\{(?:[^{}]*|(?R))*\}|\[(?:[^\[\]]*|(?R))*\])"""
pattern = regex.compile(json_pattern, regex.VERBOSE)
matches = pattern.findall(text)

matches = [fix_json(m) for m in matches]
json_pattern = r"""(?:\{(?:[^{}]*|(?R))*\}|\[(?:[^\[\]]*|(?R))*\])"""
pattern = regex.compile(json_pattern, regex.VERBOSE)
matches = pattern.findall(text)
matches = [fix_json(match) for match in matches]
return matches


Expand All @@ -231,28 +204,50 @@ def parse_xml_from_text(text: str, label: str) -> List[str]:
values = [match.strip() for match in matches]
return values

def parse_data_from_text(text: str, datatype: str):

if datatype == "str":
def parse_data_from_text(text: str, datatype: Type):
if datatype is str:
data = text
elif datatype == "int":

elif datatype is int:
data = int(text)
elif datatype == "float":

elif datatype is float:
data = float(text)
elif datatype == "bool":

elif datatype is bool:
data = text.lower() in ("true", "yes", "1", "on", "True")
elif datatype == "list":
data = eval(text)
elif datatype == "dict":
data = eval(text)

elif datatype is list:
try:
data = json.loads(text)
except json.JSONDecodeError:
data = [item.strip() for item in text.split(",")]
type_args = get_args(datatype)
if len(type_args) == 1:
data = [parse_data_from_text(item, type_args[0]) for item in data]

elif datatype is dict:
data = json.loads(text)

elif get_origin(datatype) is Union or get_origin(datatype) is UnionType:
type_args = get_args(datatype)
for i, type_arg in enumerate(type_args):
try:
data = parse_data_from_text(text, type_arg)
break
except Exception:
if i == len(type_args) - 1:
data = text
continue

else:
# raise ValueError(
# f"Invalid value '{datatype}' is detected for `datatype`. "
# "Available choices: ['str', 'int', 'float', 'bool', 'list', 'dict']"
# )
# logger.warning(f"Unknown datatype '{datatype}' is detected for `datatype`. Return the raw text instead.")
# failed to parse the data, return the raw text
return text
return text
return data

def parse_json_from_llm_output(text: str) -> dict:
Expand Down
Loading
Loading