diff --git a/examples b/examples index 8279319f5..1656232ed 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 8279319f5509a46ad04c053ffe5bd0c9fbd482d8 +Subproject commit 1656232ed7face05ed8916cb14f1bed78ffcc70b diff --git a/pyproject.toml b/pyproject.toml index 285e3dfe7..3f468de95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flock-core" -version = "0.4.519" +version = "0.4.525" description = "Declarative LLM Orchestration at Scale" readme = "README.md" authors = [ @@ -45,7 +45,7 @@ dependencies = [ "thefuzz>=0.22.1", "tiktoken>=0.8.0", "toml>=0.10.2", - "tqdm>=4.67.1", + "tqdm>=4.60.1", "uvicorn>=0.34.0", "aiosqlite>=0.21.0", "markdown2>=2.5.3", @@ -54,15 +54,15 @@ dependencies = [ "opik>=1.7.26", "azure-data-tables>=12.7.0", "croniter>=6.0.0", - ] [project.optional-dependencies] basic-tools = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", + ] azure-tools = [ "azure-identity>=1.23.0", @@ -83,7 +83,7 @@ evaluation = [ "sentence-transformers>=3.4.1", ] all-tools = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", @@ -94,7 +94,7 @@ all-tools = [ "docker>=7.1.0", ] all = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", @@ -214,3 +214,11 @@ docs = ["_docs-build", "_docs-serve"] [tool.poe.tasks.clean] script = "poethepoet.scripts:rm('dist', 'htmlcov', 'logs','metrics','.mypy_cache', '.pytest_cache', './**/__pycache__')" + +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true \ No newline at end of file diff --git a/src/flock/core/flock_factory.py b/src/flock/core/flock_factory.py index 43971bf6c..a3aa59547 100644 --- a/src/flock/core/flock_factory.py +++ b/src/flock/core/flock_factory.py @@ -413,6 +413,7 @@ def create_default_agent( write_to_file: bool = False, stream: bool = False, include_thought_process: bool = False, + include_reasoning: bool = False, temporal_activity_config: TemporalActivityConfig | None = None, ) -> FlockAgent: """Creates a default FlockAgent. @@ -433,6 +434,7 @@ def create_default_agent( max_retries=max_retries, stream=stream, include_thought_process=include_thought_process, + include_reasoning=include_reasoning, ) evaluator = DeclarativeEvaluator(name="default", config=eval_config) diff --git a/src/flock/evaluators/declarative/declarative_evaluator.py b/src/flock/evaluators/declarative/declarative_evaluator.py index 1e89dd21c..ef624eb65 100644 --- a/src/flock/evaluators/declarative/declarative_evaluator.py +++ b/src/flock/evaluators/declarative/declarative_evaluator.py @@ -36,6 +36,10 @@ class DeclarativeEvaluatorConfig(FlockEvaluatorConfig): default=False, description="Include the thought process in the output.", ) + include_reasoning: bool = Field( + default=False, + description="Include the reasoning in the output.", + ) kwargs: dict[str, Any] = Field(default_factory=dict) @@ -154,6 +158,9 @@ async def evaluate( self._lm_history = lm_history console.print("\n") + result_dict = self.filter_reasoning( + result_dict, self.config.include_reasoning + ) return self.filter_thought_process( result_dict, self.config.include_thought_process ) @@ -170,6 +177,9 @@ async def evaluate( ) self._cost = cost self._lm_history = lm_history + result_dict = self.filter_reasoning( + result_dict, self.config.include_reasoning + ) return self.filter_thought_process( result_dict, self.config.include_thought_process ) @@ -190,5 +200,18 @@ def filter_thought_process( return { k: v for k, v in result_dict.items() - if not (k.startswith("reasoning") or k.startswith("trajectory")) + if not (k.startswith("trajectory")) + } + + def filter_reasoning( + self, result_dict: dict[str, Any], include_reasoning: bool + ) -> dict[str, Any]: + """Filter out reasoning from the result dictionary.""" + if include_reasoning: + return result_dict + else: + return { + k: v + for k, v in result_dict.items() + if not (k.startswith("reasoning")) } diff --git a/src/flock/routers/conditional/conditional_router.py b/src/flock/routers/conditional/conditional_router.py index a07a85545..bf70c9c90 100644 --- a/src/flock/routers/conditional/conditional_router.py +++ b/src/flock/routers/conditional/conditional_router.py @@ -115,6 +115,10 @@ class ConditionalRouterConfig(FlockRouterConfig): default="flock.assertion_feedback", # Useful if paired with AssertionCheckerModule description="Optional context key containing feedback message to potentially include when retrying.", ) + feedback_on_failure: str | None = Field( + default=None, + description="Default feedback message to use when condition evaluation fails.", + ) retry_count_context_key_prefix: str = Field( default="flock.conditional_retry_count_", description="Internal prefix for context key storing retry attempts per agent.", diff --git a/src/flock/webapp/app/api/execution.py b/src/flock/webapp/app/api/execution.py index 7008b7978..2389d627c 100644 --- a/src/flock/webapp/app/api/execution.py +++ b/src/flock/webapp/app/api/execution.py @@ -10,6 +10,7 @@ Form, Request, ) +from fastapi.encoders import jsonable_encoder from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates @@ -151,7 +152,13 @@ async def htmx_run_flock( return HTMLResponse(f"

Error processing inputs for {start_agent_name}: {e_parse}

") result_data = await run_current_flock_service(start_agent_name, inputs, request.app.state) - raw_json_for_template = json.dumps(result_data, indent=2) + + + raw_json_for_template = json.dumps( + jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc. + indent=2, + ensure_ascii=False + ) # Unescape newlines for proper display in HTML
 tag
     result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
     root_path = request.scope.get("root_path", "")
@@ -215,7 +222,11 @@ async def htmx_run_shared_flock(
 
         shared_logger.info(f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}")
         result_data = await temp_flock.run_async(start_agent=start_agent_name, input=inputs, box_result=False)
-        raw_json_for_template = json.dumps(result_data, indent=2)
+        raw_json_for_template = json.dumps(
+            jsonable_encoder(result_data),   # ← converts every nested BaseModel, datetime, etc.
+            indent=2,
+            ensure_ascii=False
+        )
         # Unescape newlines for proper display in HTML 
 tag
         result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
         shared_logger.info(f"HTMX Run Shared: Agent '{start_agent_name}' executed. Result keys: {list(result_data.keys()) if isinstance(result_data, dict) else 'N/A'}")
diff --git a/src/flock/webapp/app/services/sharing_store.py b/src/flock/webapp/app/services/sharing_store.py
index 412624979..f9d311c3d 100644
--- a/src/flock/webapp/app/services/sharing_store.py
+++ b/src/flock/webapp/app/services/sharing_store.py
@@ -4,6 +4,7 @@
 import sqlite3
 from abc import ABC, abstractmethod
 from pathlib import Path
+from typing import Any
 
 import aiosqlite
 
@@ -228,135 +229,181 @@ async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
             logger.error(f"SQLite error saving feedback {record.feedback_id}: {e}", exc_info=True)
             raise
 
+# ---------------------------------------------------------------------------
+# Azure Table + Blob implementation
+# ---------------------------------------------------------------------------
+
+try:
+    from azure.storage.blob.aio import BlobServiceClient
+    AZURE_BLOB_AVAILABLE = True
+except ImportError:  # blob SDK not installed
+    AZURE_BLOB_AVAILABLE = False
+    BlobServiceClient = None
+
 class AzureTableSharedLinkStore(SharedLinkStoreInterface):
-    """Azure Table Storage implementation for storing and retrieving shared link configurations."""
+    """Store configs in Azure Table; store large flock YAML in Blob Storage."""
+
+    _TABLE_NAME        = "flocksharedlinks"
+    _FEEDBACK_TBL_NAME = "flockfeedback"
+    _CONTAINER_NAME    = "flocksharedlinkdefs"          # blobs live here
+    _PARTITION_KEY     = "shared_links"
 
     def __init__(self, connection_string: str):
-        """Initialize Azure Table Storage store with connection string."""
         if not AZURE_AVAILABLE:
-            raise ImportError("Azure Table Storage dependencies not available. Install with: pip install azure-data-tables")
+            raise ImportError("pip install azure-data-tables")
+        if not AZURE_BLOB_AVAILABLE:
+            raise ImportError("pip install azure-storage-blob")
 
         self.connection_string = connection_string
-        self.table_service_client = TableServiceClient.from_connection_string(connection_string)
-        self.shared_links_table_name = "flocksharedlinks"
-        self.feedback_table_name = "flockfeedback"
-        logger.info("AzureTableSharedLinkStore initialized")
+        self.table_svc = TableServiceClient.from_connection_string(connection_string)
+        self.blob_svc  = BlobServiceClient.from_connection_string(connection_string)
 
+    # ------------------------------------------------------------------ init
     async def initialize(self) -> None:
-        """Initializes the Azure Tables (creates them if they don't exist)."""
+        # 1. Azure Tables ----------------------------------------------------
         try:
-            # Create shared_links table
-            try:
-                await self.table_service_client.create_table(self.shared_links_table_name)
-                logger.info(f"Created Azure Table: {self.shared_links_table_name}")
-            except ResourceExistsError:
-                logger.debug(f"Azure Table already exists: {self.shared_links_table_name}")
-
-            # Create feedback table
-            try:
-                await self.table_service_client.create_table(self.feedback_table_name)
-                logger.info(f"Created Azure Table: {self.feedback_table_name}")
-            except ResourceExistsError:
-                logger.debug(f"Azure Table already exists: {self.feedback_table_name}")
-
-            logger.info("Azure Table Storage initialized successfully")
-        except Exception as e:
-            logger.error(f"Error initializing Azure Table Storage: {e}", exc_info=True)
-            raise
+            await self.table_svc.create_table(self._TABLE_NAME)
+            logger.info("Created Azure Table '%s'", self._TABLE_NAME)
+        except ResourceExistsError:
+            logger.debug("Azure Table '%s' already exists", self._TABLE_NAME)
 
-    async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
-        """Saves a shared link configuration to Azure Table Storage."""
         try:
-            table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
-            entity = {
-                "PartitionKey": "shared_links",  # Use a fixed partition key for simplicity
-                "RowKey": config.share_id,
-                "share_id": config.share_id,
-                "agent_name": config.agent_name,
-                "flock_definition": config.flock_definition,
-                "created_at": config.created_at.isoformat(),
-                "share_type": config.share_type,
-                "chat_message_key": config.chat_message_key,
-                "chat_history_key": config.chat_history_key,
-                "chat_response_key": config.chat_response_key,
-            }
-
-            await table_client.upsert_entity(entity)
-            logger.info(f"Saved shared link config to Azure Table Storage for ID: {config.share_id} with type: {config.share_type}")
-            return config
-        except Exception as e:
-            logger.error(f"Error saving config to Azure Table Storage for ID {config.share_id}: {e}", exc_info=True)
-            raise
+            await self.table_svc.create_table(self._FEEDBACK_TBL_NAME)
+            logger.info("Created Azure Table '%s'", self._FEEDBACK_TBL_NAME)
+        except ResourceExistsError:
+            logger.debug("Azure Table '%s' already exists", self._FEEDBACK_TBL_NAME)
+
+        # 2. Blob container --------------------------------------------------
+        try:
+            await self.blob_svc.create_container(self._CONTAINER_NAME)
+            logger.info("Created Blob container '%s'", self._CONTAINER_NAME)
+        except ResourceExistsError:
+            logger.debug("Blob container '%s' already exists", self._CONTAINER_NAME)
 
+    # ------------------------------------------------------------- save_config
+    async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
+        """Upload YAML to Blob, then upsert table row containing the blob name."""
+        blob_name   = f"{config.share_id}.yaml"
+        blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+
+        # 1. Upload flock_definition (overwrite in case of retry)
+        await blob_client.upload_blob(config.flock_definition,
+                                      overwrite=True,
+                                      content_type="text/yaml")
+        logger.debug("Uploaded blob '%s' (%d bytes)",
+                     blob_name, len(config.flock_definition.encode()))
+
+        # 2. Persist lightweight record in the table
+        tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
+        entity = {
+            "PartitionKey": self._PARTITION_KEY,
+            "RowKey":       config.share_id,
+            "agent_name":   config.agent_name,
+            "created_at":   config.created_at.isoformat(),
+            "share_type":   config.share_type,
+            "chat_message_key":  config.chat_message_key,
+            "chat_history_key":  config.chat_history_key,
+            "chat_response_key": config.chat_response_key,
+            # NEW – just a few bytes, well under 64 KiB
+            "flock_blob_name": blob_name,
+        }
+        await tbl_client.upsert_entity(entity)
+        logger.info("Saved shared link %s → blob '%s'", config.share_id, blob_name)
+        return config
+
+    # -------------------------------------------------------------- get_config
     async def get_config(self, share_id: str) -> SharedLinkConfig | None:
-        """Retrieves a shared link configuration from Azure Table Storage by its ID."""
+        tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
         try:
-            table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
-            entity = await table_client.get_entity(partition_key="shared_links", row_key=share_id)
-
-            logger.debug(f"Retrieved shared link config from Azure Table Storage for ID: {share_id}")
-            return SharedLinkConfig(
-                share_id=entity["share_id"],
-                agent_name=entity["agent_name"],
-                created_at=entity["created_at"],  # Pydantic will parse from ISO format
-                flock_definition=entity["flock_definition"],
-                share_type=entity.get("share_type", "agent_run"),
-                chat_message_key=entity.get("chat_message_key"),
-                chat_history_key=entity.get("chat_history_key"),
-                chat_response_key=entity.get("chat_response_key"),
-            )
+            entity = await tbl_client.get_entity(self._PARTITION_KEY, share_id)
         except ResourceNotFoundError:
-            logger.debug(f"No shared link config found in Azure Table Storage for ID: {share_id}")
+            logger.debug("No config entity for id '%s'", share_id)
             return None
+
+        blob_name   = entity["flock_blob_name"]
+        blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+        try:
+            blob_bytes = await (await blob_client.download_blob()).readall()
+            flock_yaml = blob_bytes.decode()
         except Exception as e:
-            logger.error(f"Error retrieving config from Azure Table Storage for ID {share_id}: {e}", exc_info=True)
-            return None
+            logger.error("Cannot download blob '%s' for share_id=%s: %s",
+                         blob_name, share_id, e, exc_info=True)
+            raise
 
+        return SharedLinkConfig(
+            share_id          = share_id,
+            agent_name        = entity["agent_name"],
+            created_at        = entity["created_at"],
+            flock_definition  = flock_yaml,
+            share_type        = entity.get("share_type", "agent_run"),
+            chat_message_key  = entity.get("chat_message_key"),
+            chat_history_key  = entity.get("chat_history_key"),
+            chat_response_key = entity.get("chat_response_key"),
+        )
+
+    # ----------------------------------------------------------- delete_config
     async def delete_config(self, share_id: str) -> bool:
-        """Deletes a shared link configuration from Azure Table Storage by its ID."""
+        tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
         try:
-            table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
-            await table_client.delete_entity(partition_key="shared_links", row_key=share_id)
-            logger.info(f"Deleted shared link config from Azure Table Storage for ID: {share_id}")
-            return True
+            entity = await tbl_client.get_entity(self._PARTITION_KEY, share_id)
         except ResourceNotFoundError:
-            logger.info(f"Attempted to delete non-existent shared link config from Azure Table Storage for ID: {share_id}")
-            return False
-        except Exception as e:
-            logger.error(f"Error deleting config from Azure Table Storage for ID {share_id}: {e}", exc_info=True)
+            logger.info("Delete: entity %s not found", share_id)
             return False
 
-    # ----------------------- Feedback methods -----------------------
+        # 1. Remove blob (ignore missing blob)
+        blob_name   = entity["flock_blob_name"]
+        blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+        try:
+            await blob_client.delete_blob(delete_snapshots="include")
+            logger.debug("Deleted blob '%s'", blob_name)
+        except ResourceNotFoundError:
+            logger.warning("Blob '%s' already gone", blob_name)
+
+        # 2. Remove table row
+        await tbl_client.delete_entity(self._PARTITION_KEY, share_id)
+        logger.info("Deleted shared link %s and its blob", share_id)
+        return True
 
+    # -------------------------------------------------------- save_feedback --
     async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
-        """Persist a feedback record to Azure Table Storage."""
-        try:
-            table_client = self.table_service_client.get_table_client(self.feedback_table_name)
-
-            entity = {
-                "PartitionKey": "feedback",  # Use a fixed partition key for simplicity
-                "RowKey": record.feedback_id,
-                "feedback_id": record.feedback_id,
-                "share_id": record.share_id,
-                "context_type": record.context_type,
-                "reason": record.reason,
-                "expected_response": record.expected_response,
-                "actual_response": record.actual_response,
-                "flock_name": record.flock_name,
-                "agent_name": record.agent_name,
-                "flock_definition": record.flock_definition,
-                "created_at": record.created_at.isoformat(),
-            }
-
-            await table_client.upsert_entity(entity)
-            logger.info(f"Saved feedback to Azure Table Storage: {record.feedback_id} (share={record.share_id})")
-            return record
-        except Exception as e:
-            logger.error(f"Error saving feedback to Azure Table Storage {record.feedback_id}: {e}", exc_info=True)
-            raise
+        """Persist a feedback record. If a flock_definition is present, upload it as a blob and
+        store only a reference in the table row to avoid oversized entities (64 KiB limit).
+        """
+        tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
+
+        # Core entity fields (avoid dumping the full Pydantic model – too many columns / large value)
+        entity: dict[str, Any] = {
+            "PartitionKey": "feedback",
+            "RowKey":       record.feedback_id,
+            "share_id":     record.share_id,
+            "context_type": record.context_type,
+            "reason":       record.reason,
+            "expected_response": record.expected_response,
+            "actual_response":   record.actual_response,
+            "created_at":   record.created_at.isoformat(),
+        }
+        if record.flock_name is not None:
+            entity["flock_name"] = record.flock_name
+        if record.agent_name is not None:
+            entity["agent_name"] = record.agent_name
+
+        # ------------------------------------------------------------------ YAML → Blob
+        if record.flock_definition:
+            blob_name   = f"{record.feedback_id}.yaml"
+            blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+            # Overwrite=true so repeated feedback_id uploads (shouldn't happen) won't error
+            await blob_client.upload_blob(record.flock_definition,
+                                          overwrite=True,
+                                          content_type="text/yaml")
+            entity["flock_blob_name"] = blob_name  # lightweight reference only
+
+        # ------------------------------------------------------------------ Table upsert
+        await tbl_client.upsert_entity(entity)
+        logger.info("Saved feedback %s%s",
+                    record.feedback_id,
+                    f" → blob '{entity['flock_blob_name']}'" if "flock_blob_name" in entity else "")
+        return record
+
 
 
 # ----------------------- Factory Function -----------------------
diff --git a/uv.lock b/uv.lock
index d0b4d9c29..7dd3736f5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1005,7 +1005,7 @@ wheels = [
 
 [[package]]
 name = "docling"
-version = "2.33.0"
+version = "2.34.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "beautifulsoup4" },
@@ -1035,9 +1035,9 @@ dependencies = [
     { name = "tqdm" },
     { name = "typer" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/36/d8/76740f7d40a28794365e76029e7379f7d1b5994e24aaac899c3bb280e426/docling-2.33.0.tar.gz", hash = "sha256:40c27df6a7f90b8fb4279a4094ac26c48f3dbd38ceda0d3208e6d63eda2e8660", size = 132120 }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/42/33334eeaa3d42a4b3406adc7bbef8c655d026c1c41ad0a8fa858bbb7ef1b/docling-2.34.0.tar.gz", hash = "sha256:52ad3c79b7e56e978fcdd8f2040fe739584e25e1c2fd5f9519fe3d51002de0d2", size = 135016 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/9c/23/6dddf454610c4dff5799f9becbbc752b8595169de57c02a1d39067f5f2b5/docling-2.33.0-py3-none-any.whl", hash = "sha256:6c0ee223c5da551adc5da15aae205aaebf78222780198cb5a21c85648da618ba", size = 169441 },
+    { url = "https://files.pythonhosted.org/packages/be/b6/b855f19ab37a6f92b60a31a7db57160048f88a980aef4b97a5c260860ed4/docling-2.34.0-py3-none-any.whl", hash = "sha256:a69c368382cc824a5a5ad21882b8772f85117a25c81257a57d54582bf38b2810", size = 173153 },
 ]
 
 [[package]]
@@ -1282,7 +1282,7 @@ wheels = [
 
 [[package]]
 name = "flock-core"
-version = "0.4.518"
+version = "0.4.524"
 source = { editable = "." }
 dependencies = [
     { name = "aiosqlite" },
@@ -1426,9 +1426,9 @@ requires-dist = [
     { name = "devtools", specifier = ">=0.12.2" },
     { name = "docker", marker = "extra == 'all-tools'", specifier = ">=7.1.0" },
     { name = "docker", marker = "extra == 'code-tools'", specifier = ">=7.1.0" },
-    { name = "docling", marker = "extra == 'all'", specifier = ">=2.18.0" },
-    { name = "docling", marker = "extra == 'all-tools'", specifier = ">=2.18.0" },
-    { name = "docling", marker = "extra == 'basic-tools'", specifier = ">=2.18.0" },
+    { name = "docling", marker = "extra == 'all'", specifier = ">=2.34.0" },
+    { name = "docling", marker = "extra == 'all-tools'", specifier = ">=2.34.0" },
+    { name = "docling", marker = "extra == 'basic-tools'", specifier = ">=2.34.0" },
     { name = "dspy", specifier = "==2.6.23" },
     { name = "duckduckgo-search", marker = "extra == 'all'", specifier = ">=7.3.2" },
     { name = "duckduckgo-search", marker = "extra == 'all-tools'", specifier = ">=7.3.2" },
@@ -1481,7 +1481,7 @@ requires-dist = [
     { name = "thefuzz", specifier = ">=0.22.1" },
     { name = "tiktoken", specifier = ">=0.8.0" },
     { name = "toml", specifier = ">=0.10.2" },
-    { name = "tqdm", specifier = ">=4.67.1" },
+    { name = "tqdm", specifier = ">=4.60.1" },
     { name = "uvicorn", specifier = ">=0.34.0" },
     { name = "wd-di", specifier = ">=0.2.14" },
     { name = "websockets", specifier = ">=15.0.1" },