diff --git a/README.md b/README.md index 207e9258..d2093d4f 100644 --- a/README.md +++ b/README.md @@ -209,15 +209,12 @@ print(f'Structured Response: {response}') Define your agents using YAML for easy configuration and deployment: ```yaml -apiVersion: flo/alpha-v1 -kind: FloAgent metadata: name: email-summary-flo version: 1.0.0 description: "Agent for analyzing email threads" agent: name: EmailSummaryAgent - kind: llm role: Email communication expert model: provider: openai diff --git a/flo_ai/examples/yaml_agent_example.py b/flo_ai/examples/yaml_agent_example.py index d50e4c45..e306bfd5 100644 --- a/flo_ai/examples/yaml_agent_example.py +++ b/flo_ai/examples/yaml_agent_example.py @@ -2,19 +2,16 @@ # Example YAML configuration yaml_config = """ -apiVersion: flo/alpha-v1 metadata: - name: email-summary-flo version: 1.0.0 description: "Agent for analyzing email threads between customers and support" tags: ["email", "analysis", "support"] agent: name: EmailSummaryAgent - kind: llm role: Email communication expert model: - provider: openai # or claude - name: gpt-4o-mini # or claude-3-5-sonnet-20240620 + provider: anthropic # or claude + name: claude-3-5-sonnet-20240620 settings: temperature: 0 max_retries: 3 diff --git a/flo_ai/flo_ai/__init__.py b/flo_ai/flo_ai/__init__.py index e69de29b..b0a3ace4 100644 --- a/flo_ai/flo_ai/__init__.py +++ b/flo_ai/flo_ai/__init__.py @@ -0,0 +1,66 @@ +""" +flo_ai - A flexible agent framework for LLM-powered applications +""" + +# Models package - Agent framework components +from .models import Agent, AgentError, BaseAgent, AgentType, ReasoningPattern + +from .builder.agent_builder import AgentBuilder + +# LLM package - Language model integrations +from .llm import BaseLLM, Anthropic, OpenAI, OllamaLLM, Gemini + +# Tool package - Tool framework components +from .tool import Tool, ToolExecutionError, flo_tool, create_tool_from_function + +# Arium package - Workflow and memory components +from .arium import ( + Arium, + BaseArium, + create_arium, + MessageMemory, + BaseMemory, + StartNode, + EndNode, + Edge, +) + +# Utils package - Utility functions +from .utils import FloUtils + +__all__ = [ + # Models + 'Agent', + 'AgentError', + 'BaseAgent', + 'AgentType', + 'ReasoningPattern', + # Utils + 'FloUtils', + # LLM + 'BaseLLM', + 'Anthropic', + 'OpenAI', + 'OllamaLLM', + 'Gemini', + # Tools + 'Tool', + 'ToolExecutionError', + 'flo_tool', + 'create_tool_from_function', + # Arium + 'Arium', + 'BaseArium', + 'AriumBuilder', + 'create_arium', + 'MessageMemory', + 'BaseMemory', + 'StartNode', + 'EndNode', + 'Edge', + # Builder + 'AgentBuilder', + 'AriumBuilder', +] + +__version__ = '1.0.0' diff --git a/flo_ai/flo_ai/builder/agent_builder.py b/flo_ai/flo_ai/builder/agent_builder.py index 583095a8..50ffeb83 100644 --- a/flo_ai/flo_ai/builder/agent_builder.py +++ b/flo_ai/flo_ai/builder/agent_builder.py @@ -94,7 +94,10 @@ def build(self) -> Agent: @classmethod def from_yaml( - cls, yaml_str: str, tools: Optional[List[Tool]] = None + cls, + yaml_str: str, + tools: Optional[List[Tool]] = None, + base_llm: Optional[BaseLLM] = None, ) -> 'AgentBuilder': """Create an agent builder from a YAML configuration string @@ -119,7 +122,8 @@ def from_yaml( builder.with_role(agent_config.get('role')) # Configure LLM based on model settings - if 'model' in agent_config: + if 'model' in agent_config and base_llm is None: + base_url = agent_config.get('base_url', None) model_config = agent_config['model'] provider = model_config.get('provider', 'openai').lower() model_name = model_config.get('name') @@ -128,15 +132,21 @@ def from_yaml( raise ValueError('Model name must be specified in YAML configuration') if provider == 'openai': - builder.with_llm(OpenAI(model=model_name)) - elif provider == 'claude': - builder.with_llm(Anthropic(model=model_name)) + builder.with_llm(OpenAI(model=model_name, base_url=base_url)) + elif provider == 'anthropic': + builder.with_llm(Anthropic(model=model_name, base_url=base_url)) elif provider == 'gemini': - builder.with_llm(Gemini(model=model_name)) + builder.with_llm(Gemini(model=model_name, base_url=base_url)) elif provider == 'ollama': - builder.with_llm(OllamaLLM(model=model_name)) + builder.with_llm(OllamaLLM(model=model_name, base_url=base_url)) else: raise ValueError(f'Unsupported model provider: {provider}') + else: + if base_llm is None: + raise ValueError( + 'Model must be specified in YAML configuration or base_llm must be provided' + ) + builder.with_llm(base_llm) # Set tools if provided if tools: diff --git a/flo_ai/flo_ai/llm/anthropic_llm.py b/flo_ai/flo_ai/llm/anthropic_llm.py index b8234489..74685397 100644 --- a/flo_ai/flo_ai/llm/anthropic_llm.py +++ b/flo_ai/flo_ai/llm/anthropic_llm.py @@ -11,10 +11,11 @@ def __init__( model: str = 'claude-3-5-sonnet-20240620', temperature: float = 0.7, api_key: Optional[str] = None, + base_url: str = None, **kwargs, ): super().__init__(model, api_key, temperature, **kwargs) - self.client = AsyncAnthropic(api_key=self.api_key) + self.client = AsyncAnthropic(api_key=self.api_key, base_url=base_url) async def generate( self, diff --git a/flo_ai/flo_ai/llm/base_llm.py b/flo_ai/flo_ai/llm/base_llm.py index 2a5888df..8cf435f5 100644 --- a/flo_ai/flo_ai/llm/base_llm.py +++ b/flo_ai/flo_ai/llm/base_llm.py @@ -7,6 +7,7 @@ @dataclass class ImageMessage: image_url: Optional[str] = None + image_bytes: Optional[bytes] = None image_file_path: Optional[str] = None image_base64: Optional[str] = None mime_type: Optional[str] = None diff --git a/flo_ai/flo_ai/llm/gemini_llm.py b/flo_ai/flo_ai/llm/gemini_llm.py index 94670620..89151ff0 100644 --- a/flo_ai/flo_ai/llm/gemini_llm.py +++ b/flo_ai/flo_ai/llm/gemini_llm.py @@ -12,6 +12,7 @@ def __init__( model: str = 'gemini-2.5-flash', temperature: float = 0.7, api_key: Optional[str] = None, + base_url: str = None, **kwargs, ): super().__init__(model, api_key, temperature, **kwargs) @@ -153,6 +154,11 @@ def format_image_in_message(self, image: ImageMessage) -> str: data=image_bytes, mime_type=image.mime_type, ) + elif image.image_bytes: + return genai.types.Part.from_bytes( + data=image.image_bytes, + mime_type=image.mime_type, + ) raise NotImplementedError( 'Not other way other than file path has been implemented' ) diff --git a/flo_ai/flo_ai/llm/openai_llm.py b/flo_ai/flo_ai/llm/openai_llm.py index 10c2ade0..ebbb9128 100644 --- a/flo_ai/flo_ai/llm/openai_llm.py +++ b/flo_ai/flo_ai/llm/openai_llm.py @@ -10,12 +10,13 @@ def __init__( model='gpt-40-mini', api_key: str = None, temperature: float = 0.7, + base_url: str = None, **kwargs, ): super().__init__( model=model, api_key=api_key, temperature=temperature, **kwargs ) - self.client = AsyncOpenAI(api_key=api_key, **kwargs) + self.client = AsyncOpenAI(api_key=api_key, base_url=base_url) self.model = model self.kwargs = kwargs diff --git a/flo_ai/flo_ai/models/__init__.py b/flo_ai/flo_ai/models/__init__.py index e69de29b..52f0ceb6 100644 --- a/flo_ai/flo_ai/models/__init__.py +++ b/flo_ai/flo_ai/models/__init__.py @@ -0,0 +1,9 @@ +""" +Models package for flo_ai - Agent framework components +""" + +from .agent import Agent +from .agent_error import AgentError +from .base_agent import BaseAgent, AgentType, ReasoningPattern + +__all__ = ['Agent', 'AgentError', 'BaseAgent', 'AgentType', 'ReasoningPattern'] diff --git a/flo_ai/flo_ai/utils/__init__.py b/flo_ai/flo_ai/utils/__init__.py new file mode 100644 index 00000000..fdfeca4e --- /dev/null +++ b/flo_ai/flo_ai/utils/__init__.py @@ -0,0 +1,3 @@ +from .flo_utils import FloUtils + +__all__ = ['FloUtils'] diff --git a/flo_ai/flo_ai/utils/flo_utils.py b/flo_ai/flo_ai/utils/flo_utils.py new file mode 100644 index 00000000..2aaef06a --- /dev/null +++ b/flo_ai/flo_ai/utils/flo_utils.py @@ -0,0 +1,125 @@ +from typing import Dict, Any +import json +from flo_ai.utils.logger import logger + + +class FloUtils: + @staticmethod + def extract_jsons_from_string(data: str, strict: bool = False) -> Dict[str, Any]: + """ + 1) Find all balanced `{ … }` blocks via a custom parser + 2) Strip comments and json.loads() each + 3) Merge into one dict (later keys override earlier) + 4) On strict mode, raise FloException if no JSON found + """ + + # Custom function to find balanced braces since (?R) is not supported in Python re + def find_balanced_braces(text): + matches = [] + i = 0 + while i < len(text): + if text[i] == '{': + start = i + brace_count = 1 + j = i + 1 + in_string = False + escape_next = False + + while j < len(text) and brace_count > 0: + char = text[j] + + if escape_next: + escape_next = False + elif char == '\\' and in_string: + escape_next = True + elif char == '"' and not escape_next: + in_string = not in_string + elif not in_string: + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + + j += 1 + + if brace_count == 0: + candidate = text[start:j] + # Try to validate it's actually JSON by attempting to parse + try: + cleaned = FloUtils.strip_comments_from_string(candidate) + json.loads(cleaned) + matches.append(candidate) + i = j # Continue from after the valid JSON + except json.JSONDecodeError: + # Not valid JSON, try starting from the next character + i += 1 + else: + # Unbalanced braces, try starting from the next character + i += 1 + else: + i += 1 + return matches + + matches = find_balanced_braces(data) + merged: Dict[str, Any] = {} + + for json_str in matches: + try: + cleaned = FloUtils.strip_comments_from_string(json_str) + obj = json.loads(cleaned) + merged.update(obj) + except json.JSONDecodeError as e: + logger.error(f'Invalid JSON in response: {json_str}, {e}') + + if strict and not matches: + logger.error(f'No JSON found in strict mode: {data}') + raise ValueError(f'No JSON found in strict mode: {data}') + + return merged + + @staticmethod + def strip_comments_from_string(data: str) -> str: + """Remove JS-style comments (// and /*…*/) so json.loads() will succeed.""" + cleaned = [] + i = 0 + length = len(data) + + while i < length: + char = data[i] + + if char not in '"/*': + cleaned.append(char) + i += 1 + continue + + if char == '"': + cleaned.append(char) + i += 1 + while i < length: + char = data[i] + cleaned.append(char) + i += 1 + if char == '"' and (i < 2 or data[i - 2] != '\\'): + break + continue + + if char == '/' and i + 1 < length: + next_char = data[i + 1] + if next_char == '/': + i += 2 + while i < length and data[i] != '\n': + i += 1 + continue + elif next_char == '*': + i += 2 + while i + 1 < length: + if data[i] == '*' and data[i + 1] == '/': + i += 2 + break + i += 1 + continue + + cleaned.append(char) + i += 1 + + return ''.join(cleaned) diff --git a/flo_ai/poetry.lock b/flo_ai/poetry.lock index 4160c43a..baae925b 100644 --- a/flo_ai/poetry.lock +++ b/flo_ai/poetry.lock @@ -247,18 +247,6 @@ files = [ {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, ] -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - [[package]] name = "asttokens" version = "3.0.0" @@ -652,9 +640,10 @@ test = ["pytest"] name = "contourpy" version = "1.3.2" description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, @@ -729,9 +718,10 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" name = "cycler" version = "0.12.1" description = "Composable style cycles" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -892,30 +882,14 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] -[[package]] -name = "flo-ai-tools" -version = "0.0.1" -description = "Some good tool implementations for flo-ai" -optional = false -python-versions = ">=3.9,<4.0" -groups = ["dev"] -files = [] -develop = true - -[package.dependencies] -redshift-connector = "^2.1.5" - -[package.source] -type = "directory" -url = "../flo_ai_tools" - [[package]] name = "fonttools" version = "4.58.5" description = "Tools to manipulate font files" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "fonttools-4.58.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d500d399aa4e92d969a0d21052696fa762385bb23c3e733703af4a195ad9f34c"}, {file = "fonttools-4.58.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b00530b84f87792891874938bd42f47af2f7f4c2a1d70466e6eb7166577853ab"}, @@ -1622,9 +1596,10 @@ test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout" name = "kiwisolver" version = "1.4.8" description = "A fast implementation of the Cassowary constraint solver" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, @@ -1708,155 +1683,6 @@ files = [ {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, ] -[[package]] -name = "lxml" -version = "5.4.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, - {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, - {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, - {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, - {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, - {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, - {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, - {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, - {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, - {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, - {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, - {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, - {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, - {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, - {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, - {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, - {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, - {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, - {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, - {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, - {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, - {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, - {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, - {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, - {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, - {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, - {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, - {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, - {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, - {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, - {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, - {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, - {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, - {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, - {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml_html_clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11,<3.1.0)"] - [[package]] name = "markupsafe" version = "3.0.2" @@ -1932,9 +1758,10 @@ files = [ name = "matplotlib" version = "3.10.3" description = "Python plotting package" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7"}, {file = "matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb"}, @@ -2165,10 +1992,10 @@ files = [ name = "networkx" version = "3.4.2" description = "Python package for creating and manipulating graphs and networks" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version == \"3.10\"" +markers = "python_version == \"3.10\" and extra == \"vizualize\"" files = [ {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, @@ -2186,10 +2013,10 @@ test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] name = "networkx" version = "3.5" description = "Python package for creating and manipulating graphs and networks" -optional = false +optional = true python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.11\"" +markers = "python_version >= \"3.11\" and extra == \"vizualize\"" files = [ {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, @@ -2261,6 +2088,7 @@ files = [ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +markers = {main = "extra == \"vizualize\""} [[package]] name = "openai" @@ -2301,6 +2129,7 @@ files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +markers = {main = "extra == \"vizualize\""} [[package]] name = "pandas" @@ -3059,9 +2888,10 @@ windows-terminal = ["colorama (>=0.4.6)"] name = "pyparsing" version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"vizualize\"" files = [ {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, @@ -3146,6 +2976,7 @@ files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] +markers = {main = "extra == \"vizualize\""} [package.dependencies] six = ">=1.5" @@ -3359,31 +3190,6 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} -[[package]] -name = "redshift-connector" -version = "2.1.8" -description = "Redshift interface library" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "redshift_connector-2.1.8-py3-none-any.whl", hash = "sha256:160dff3720e8facb5f50f9585f3f68dd5565dd0d986e4e6a879371313da1b36e"}, -] - -[package.dependencies] -beautifulsoup4 = ">=4.7.0,<5.0.0" -boto3 = ">=1.9.201,<2.0.0" -botocore = ">=1.12.201,<2.0.0" -lxml = ">=4.6.5,<6.0.0" -packaging = "*" -pytz = ">=2020.1" -requests = ">=2.23.0,<3.0.0" -scramp = ">=1.2.0,<1.5.0" -setuptools = "*" - -[package.extras] -full = ["numpy", "pandas"] - [[package]] name = "referencing" version = "0.36.2" @@ -3610,42 +3416,6 @@ botocore = ">=1.36.0,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] -[[package]] -name = "scramp" -version = "1.4.6" -description = "An implementation of the SCRAM protocol." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "scramp-1.4.6-py3-none-any.whl", hash = "sha256:a0cf9d2b4624b69bac5432dd69fecfc55a542384fe73c3a23ed9b138cda484e1"}, - {file = "scramp-1.4.6.tar.gz", hash = "sha256:fe055ebbebf4397b9cb323fcc4b299f219cd1b03fd673ca40c97db04ac7d107e"}, -] - -[package.dependencies] -asn1crypto = ">=1.5.1" - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - [[package]] name = "six" version = "1.17.0" @@ -3657,6 +3427,7 @@ files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +markers = {main = "extra == \"vizualize\""} [[package]] name = "smmap" @@ -4227,7 +3998,10 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" +[extras] +vizualize = ["matplotlib", "networkx"] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "e4b38a75aaa025ed5769e0633c97438575c11c71c05de54f5181b9a2d050fd72" +content-hash = "df6ba1cd5dc10e0b8c7ece2f2a9eed0e41c76928760a0a5c51df13919d4e68ce" diff --git a/flo_ai/pyproject.toml b/flo_ai/pyproject.toml index c7cf9b6a..460f6118 100644 --- a/flo_ai/pyproject.toml +++ b/flo_ai/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flo_ai" -version = "0.0.7-rc1" +version = "1.0.0-dev3" description = "A easy way to create structured AI agents" authors = ["rootflo <*@rootflo.ai>"] license = "MIT" @@ -11,21 +11,22 @@ packages = [ [tool.poetry.dependencies] python = ">=3.10,<4.0" -httpx = ">=0.27.0" +httpx = ">=0.28.1" pillow = "^10.3.0" pydantic = "^2.9.2" openai = "^1.77.0" google-genai = "^1.25.0" -# TODO: make into optional dependency -matplotlib = "^3.7.0" -networkx = "^3.0" +matplotlib = { version = "^3.7.0", optional = true } +networkx = { version = "^3.0", optional = true } anthropic = "^0.57.1" aiohttp = "^3.12.14" +[tool.poetry.extras] +vizualize = ["matplotlib", "networkx"] [tool.poetry.group.dev.dependencies] -boto3 = "1.36.1" -botocore = "1.36.1" +boto3 = "^1.36.1" +botocore = "^1.36.1" pypdf = "^4.2.0" ipykernel = "^6.29.5" db-sqlite3 = "^0.0.1" @@ -35,7 +36,6 @@ wikipedia = "^1.4.0" pytest = "^8.3.3" pytest-asyncio = "^0.24.0" pre-commit = "^4.0.1" -flo-ai-tools = { path = "../flo_ai_tools", develop = true } streamlit = "^1.42.2" [build-system] diff --git a/flo_ai/setup.py b/flo_ai/setup.py index ab432d9a..1e1ad914 100644 --- a/flo_ai/setup.py +++ b/flo_ai/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='flo-ai', - version='0.0.7-rc1', + version='1.0.0-dev3', author='Rootflo', description='Create composable AI agents', long_description=long_description, diff --git a/flo_ai/tests/test_flo_utils.py b/flo_ai/tests/test_flo_utils.py new file mode 100644 index 00000000..27362994 --- /dev/null +++ b/flo_ai/tests/test_flo_utils.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +Pytest tests for the FloUtils utility methods. +""" + +import sys +import os +import pytest + +# Add the flo_ai directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from flo_ai.utils.flo_utils import FloUtils + + +class TestFloUtils: + """Test cases for FloUtils class methods.""" + + def test_strip_comments_from_string_single_line_comments(self): + """Test removal of single-line comments (//).""" + input_str = '{"name": "test", // this is a comment\n"value": 123}' + expected = '{"name": "test", \n"value": 123}' + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_from_string_multi_line_comments(self): + """Test removal of multi-line comments (/* */).""" + input_str = '{"name": "test", /* this is a\nmulti-line comment */ "value": 123}' + expected = '{"name": "test", "value": 123}' + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_from_string_mixed_comments(self): + """Test removal of both single-line and multi-line comments.""" + input_str = """{"name": "test", // single line comment + /* multi-line + comment */ "value": 123 // another single line}""" + expected = """{"name": "test", + "value": 123 """ + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_from_string_with_strings_containing_comment_chars(self): + """Test that comment characters inside strings are preserved.""" + input_str = ( + '{"url": "http://example.com", "comment": "This /* is not */ a comment"}' + ) + expected = ( + '{"url": "http://example.com", "comment": "This /* is not */ a comment"}' + ) + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_from_string_with_escaped_quotes(self): + """Test handling of escaped quotes in strings.""" + input_str = '{"message": "He said \\"Hello\\"", // comment\n"value": 42}' + expected = '{"message": "He said \\"Hello\\"", \n"value": 42}' + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_from_string_no_comments(self): + """Test that strings without comments are returned unchanged.""" + input_str = '{"name": "test", "value": 123, "active": true}' + result = FloUtils.strip_comments_from_string(input_str) + assert result == input_str + + def test_strip_comments_from_string_empty_string(self): + """Test handling of empty string.""" + result = FloUtils.strip_comments_from_string('') + assert result == '' + + def test_strip_comments_from_string_only_comments(self): + """Test string that is only comments.""" + input_str = '// just a comment\n/* and another */' + expected = '\n' + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_single_json(self): + """Test extraction of a single JSON object.""" + input_str = 'Some text {"name": "test", "value": 123} more text' + expected = {'name': 'test', 'value': 123} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_multiple_jsons(self): + """Test extraction and merging of multiple JSON objects.""" + input_str = 'Text {"name": "test"} more {"value": 123, "active": true} end' + expected = {'name': 'test', 'value': 123, 'active': True} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_overlapping_keys(self): + """Test that later JSON objects override earlier ones for same keys.""" + input_str = ( + '{"name": "first", "value": 100} {"name": "second", "extra": "data"}' + ) + expected = {'name': 'second', 'value': 100, 'extra': 'data'} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_nested_objects(self): + """Test extraction of nested JSON objects.""" + input_str = 'Data {"user": {"name": "test", "id": 123}, "active": true} end' + expected = {'user': {'name': 'test', 'id': 123}, 'active': True} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_with_comments(self): + """Test extraction of JSON with comments that need stripping.""" + input_str = """Text {"name": "test", // comment + "value": 123 /* another comment */} end""" + expected = {'name': 'test', 'value': 123} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_no_json(self): + """Test behavior when no JSON is found in non-strict mode.""" + input_str = 'This is just plain text with no JSON objects' + result = FloUtils.extract_jsons_from_string(input_str) + assert result == {} + + def test_extract_jsons_from_string_no_json_strict_mode(self): + """Test that strict mode raises ValueError when no JSON is found.""" + input_str = 'This is just plain text with no JSON objects' + with pytest.raises(ValueError, match='No JSON found in strict mode'): + FloUtils.extract_jsons_from_string(input_str, strict=True) + + def test_extract_jsons_from_string_invalid_json(self): + """Test handling of invalid JSON (should be skipped).""" + input_str = ( + 'Valid: {"name": "test"} Invalid: {invalid json} Valid: {"value": 123}' + ) + expected = {'name': 'test', 'value': 123} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_empty_string(self): + """Test behavior with empty string.""" + result = FloUtils.extract_jsons_from_string('') + assert result == {} + + def test_extract_jsons_from_string_empty_string_strict(self): + """Test strict mode with empty string.""" + with pytest.raises(ValueError, match='No JSON found in strict mode'): + FloUtils.extract_jsons_from_string('', strict=True) + + def test_extract_jsons_from_string_malformed_braces(self): + """Test handling of malformed brace structures.""" + input_str = 'Text { invalid { structure } more text {"valid": true}' + expected = {'valid': True} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_arrays(self): + """Test extraction of JSON objects containing arrays.""" + input_str = 'Data {"items": [1, 2, 3], "name": "test"} end' + expected = {'items': [1, 2, 3], 'name': 'test'} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_complex_nested(self): + """Test extraction of complex nested JSON structures.""" + input_str = """Text {"config": {"database": {"host": "localhost", "port": 5432}, + "features": ["auth", "logging"]}, "version": "1.0"} end""" + expected = { + 'config': { + 'database': {'host': 'localhost', 'port': 5432}, + 'features': ['auth', 'logging'], + }, + 'version': '1.0', + } + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_unicode_content(self): + """Test handling of Unicode content in JSON.""" + input_str = 'Text {"message": "Hello δΈ–η•Œ", "emoji": "πŸš€"} end' + expected = {'message': 'Hello δΈ–η•Œ', 'emoji': 'πŸš€'} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_extract_jsons_from_string_boolean_and_null(self): + """Test extraction of JSON with boolean and null values.""" + input_str = 'Data {"active": true, "disabled": false, "data": null} end' + expected = {'active': True, 'disabled': False, 'data': None} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected + + def test_strip_comments_incomplete_multiline_comment(self): + """Test handling of incomplete multi-line comments.""" + input_str = '{"name": "test", /* incomplete comment' + expected = '{"name": "test", t' + result = FloUtils.strip_comments_from_string(input_str) + assert result == expected + + def test_strip_comments_forward_slash_not_comment(self): + """Test that single forward slashes that don't form comments are preserved.""" + input_str = '{"url": "https://example.com/path", "ratio": "1/2"}' + result = FloUtils.strip_comments_from_string(input_str) + assert result == input_str + + def test_extract_jsons_from_string_deeply_nested(self): + """Test extraction with very deeply nested objects.""" + input_str = 'Text {"a": {"b": {"c": {"d": {"e": "deep"}}}}} end' + expected = {'a': {'b': {'c': {'d': {'e': 'deep'}}}}} + result = FloUtils.extract_jsons_from_string(input_str) + assert result == expected